Merge pull request #12 from torjusti/feat/hide-controls

Hide controls after inactivity
master
Stefan Otto 2018-07-16 14:39:07 +02:00 committed by GitHub
commit 51de7bb33d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 3532 additions and 2193 deletions

View File

@ -24,14 +24,14 @@ module.exports =
version: '15' version: '15'
} }
}, },
parser: 'babel-eslint',
parserOptions: parserOptions:
{ {
ecmaVersion: 6, ecmaVersion: 9,
sourceType: 'module', sourceType: 'module',
ecmaFeatures: ecmaFeatures:
{ {
impliedStrict: true, impliedStrict: true,
experimentalObjectRestSpread: true,
jsx: true jsx: true
} }
}, },
@ -121,7 +121,6 @@ module.exports =
'no-implicit-globals': 2, 'no-implicit-globals': 2,
'no-inner-declarations': 2, 'no-inner-declarations': 2,
'no-invalid-regexp': 2, 'no-invalid-regexp': 2,
'no-invalid-this': 2,
'no-irregular-whitespace': 2, 'no-irregular-whitespace': 2,
'no-lonely-if': 2, 'no-lonely-if': 2,
'no-mixed-operators': 2, 'no-mixed-operators': 2,

View File

@ -79,13 +79,7 @@ function bundle(options)
}) })
.transform('babelify', .transform('babelify',
{ {
presets : [ 'es2015', 'react' ], presets : [ 'env', 'react-app' ]
plugins :
[
'transform-runtime',
'transform-object-assign',
'transform-object-rest-spread'
]
}) })
.transform(envify( .transform(envify(
{ {
@ -132,21 +126,29 @@ function changeHTML(content)
gulp.task('clean', () => del(OUTPUT_DIR, { force: true })); gulp.task('clean', () => del(OUTPUT_DIR, { force: true }));
const LINTING_FILES = [
'gulpfile.js',
'lib/**/*.js',
'lib/**/*.jsx'
];
gulp.task('lint', () => gulp.task('lint', () =>
{ {
const src = return gulp.src(LINTING_FILES)
[
'gulpfile.js',
'lib/**/*.js',
'lib/**/*.jsx'
];
return gulp.src(src)
.pipe(plumber()) .pipe(plumber())
.pipe(eslint()) .pipe(eslint())
.pipe(eslint.format()); .pipe(eslint.format());
}); });
gulp.task('lint-fix', function()
{
return gulp.src(LINTING_FILES)
.pipe(plumber())
.pipe(eslint({ fix: true }))
.pipe(eslint.format())
.pipe(gulp.dest((file) => file.base));
});
gulp.task('css', () => gulp.task('css', () =>
{ {
return gulp.src('stylus/index.styl') return gulp.src('stylus/index.styl')

View File

@ -11,6 +11,24 @@ import ScreenView from './ScreenView';
class Me extends React.Component class Me extends React.Component
{ {
state = {
controlsVisible : false
};
handleMouseOver = () =>
{
this.setState({
controlsVisible : true
});
};
handleMouseOut = () =>
{
this.setState({
controlsVisible : false
});
};
constructor(props) constructor(props)
{ {
super(props); super(props);
@ -85,10 +103,15 @@ class Me extends React.Component
data-tip={tip} data-tip={tip}
data-tip-disable={!tip} data-tip-disable={!tip}
data-type='dark' data-type='dark'
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
> >
<div className={classnames('view-container', 'webcam')}> <div className={classnames('view-container', 'webcam')}>
{connected ? {connected ?
<div className='controls'> <div className={classnames('controls', {
visible : this.state.controlsVisible
})}
>
<div <div
className={classnames('button', 'mic', micState, { className={classnames('button', 'mic', micState, {
disabled : me.audioInProgress disabled : me.audioInProgress

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
@ -8,155 +8,182 @@ import * as stateActions from '../redux/stateActions';
import PeerView from './PeerView'; import PeerView from './PeerView';
import ScreenView from './ScreenView'; import ScreenView from './ScreenView';
const Peer = (props) => class Peer extends Component
{ {
const { state = {
advancedMode, controlsVisible : false
peer, };
micConsumer,
webcamConsumer,
screenConsumer,
onMuteMic,
onUnmuteMic,
onDisableWebcam,
onEnableWebcam,
onDisableScreen,
onEnableScreen,
toggleConsumerFullscreen,
style
} = props;
const micEnabled = ( handleMouseOver = () =>
Boolean(micConsumer) && {
!micConsumer.locallyPaused && this.setState({
!micConsumer.remotelyPaused controlsVisible : true
); });
};
const videoVisible = ( handleMouseOut = () =>
Boolean(webcamConsumer) && {
!webcamConsumer.locallyPaused && this.setState({
!webcamConsumer.remotelyPaused controlsVisible : false
); });
};
const screenVisible = ( render()
Boolean(screenConsumer) && {
!screenConsumer.locallyPaused && const {
!screenConsumer.remotelyPaused advancedMode,
); peer,
micConsumer,
let videoProfile; webcamConsumer,
screenConsumer,
if (webcamConsumer) onMuteMic,
videoProfile = webcamConsumer.profile; onUnmuteMic,
onDisableWebcam,
let screenProfile; onEnableWebcam,
onDisableScreen,
if (screenConsumer) onEnableScreen,
screenProfile = screenConsumer.profile; toggleConsumerFullscreen,
style
return ( } = this.props;
<div
data-component='Peer' const micEnabled = (
className={classnames({ Boolean(micConsumer) &&
screen : screenConsumer !micConsumer.locallyPaused &&
})} !micConsumer.remotelyPaused
> );
{videoVisible && !webcamConsumer.supported ?
<div className='incompatible-video'> const videoVisible = (
<p>incompatible video</p> Boolean(webcamConsumer) &&
</div> !webcamConsumer.locallyPaused &&
:null !webcamConsumer.remotelyPaused
} );
<div className={classnames('view-container', 'webcam')} style={style}> const screenVisible = (
<div className='controls'> Boolean(screenConsumer) &&
!screenConsumer.locallyPaused &&
!screenConsumer.remotelyPaused
);
let videoProfile;
if (webcamConsumer)
videoProfile = webcamConsumer.profile;
let screenProfile;
if (screenConsumer)
screenProfile = screenConsumer.profile;
return (
<div
data-component='Peer'
className={classnames({
screen : screenConsumer
})}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
>
{videoVisible && !webcamConsumer.supported ?
<div className='incompatible-video'>
<p>incompatible video</p>
</div>
:null
}
<div className={classnames('view-container', 'webcam')} style={style}>
<div <div
className={classnames('button', 'mic', { className={classnames('controls', {
on : micEnabled, visible : this.state.controlsVisible
off : !micEnabled,
disabled : peer.peerAudioInProgress
})} })}
onClick={(e) => >
{
e.stopPropagation();
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name);
}}
/>
<div
className={classnames('button', 'webcam', {
on : videoVisible,
off : !videoVisible,
disabled : peer.peerVideoInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
videoVisible ?
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
}}
/>
<div
className={classnames('button', 'fullscreen')}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerFullscreen(webcamConsumer);
}}
/>
</div>
<PeerView
advancedMode={advancedMode}
peer={peer}
audioTrack={micConsumer ? micConsumer.track : null}
videoTrack={webcamConsumer ? webcamConsumer.track : null}
videoVisible={videoVisible}
videoProfile={videoProfile}
audioCodec={micConsumer ? micConsumer.codec : null}
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
/>
</div>
{screenConsumer ?
<div className={classnames('view-container', 'screen')} style={style}>
<div className='controls'>
<div <div
className={classnames('button', 'screen', { className={classnames('button', 'mic', {
on : screenVisible, on : micEnabled,
off : !screenVisible, off : !micEnabled,
disabled : peer.peerScreenInProgress disabled : peer.peerAudioInProgress
})} })}
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
screenVisible ? micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name);
onDisableScreen(peer.name) : onEnableScreen(peer.name);
}} }}
/> />
<div
className={classnames('button', 'webcam', {
on : videoVisible,
off : !videoVisible,
disabled : peer.peerVideoInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
videoVisible ?
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
}}
/>
<div <div
className={classnames('button', 'fullscreen')} className={classnames('button', 'fullscreen')}
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
toggleConsumerFullscreen(screenConsumer); toggleConsumerFullscreen(webcamConsumer);
}} }}
/> />
</div> </div>
<ScreenView <PeerView
advancedMode={advancedMode} advancedMode={advancedMode}
screenTrack={screenConsumer ? screenConsumer.track : null} peer={peer}
screenVisible={screenVisible} audioTrack={micConsumer ? micConsumer.track : null}
screenProfile={screenProfile} videoTrack={webcamConsumer ? webcamConsumer.track : null}
screenCodec={screenConsumer ? screenConsumer.codec : null} videoVisible={videoVisible}
videoProfile={videoProfile}
audioCodec={micConsumer ? micConsumer.codec : null}
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
/> />
</div> </div>
:null
} {screenConsumer ?
</div> <div className={classnames('view-container', 'screen')} style={style}>
); <div className='controls'>
}; <div
className={classnames('button', 'screen', {
on : screenVisible,
off : !screenVisible,
disabled : peer.peerScreenInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
screenVisible ?
onDisableScreen(peer.name) : onEnableScreen(peer.name);
}}
/>
<div
className={classnames('button', 'fullscreen')}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerFullscreen(screenConsumer);
}}
/>
</div>
<ScreenView
advancedMode={advancedMode}
screenTrack={screenConsumer ? screenConsumer.track : null}
screenVisible={screenVisible}
screenProfile={screenProfile}
screenCodec={screenConsumer ? screenConsumer.codec : null}
/>
</div>
:null
}
</div>
);
}
}
Peer.propTypes = Peer.propTypes =
{ {

View File

@ -6,6 +6,7 @@ import classnames from 'classnames';
import ClipboardButton from 'react-clipboard.js'; import ClipboardButton from 'react-clipboard.js';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions'; import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions';
import { Appear } from './transitions'; import { Appear } from './transitions';
import Me from './Me'; import Me from './Me';
import Peers from './Peers'; import Peers from './Peers';
@ -14,9 +15,45 @@ import ToolAreaButton from './ToolArea/ToolAreaButton';
import ToolArea from './ToolArea/ToolArea'; import ToolArea from './ToolArea/ToolArea';
import FullScreenView from './FullScreenView'; import FullScreenView from './FullScreenView';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import { idle } from '../utils';
// Hide toolbars after 10 seconds of inactivity.
const TIMEOUT = 10 * 1000;
class Room extends React.Component class Room extends React.Component
{ {
/**
* Hides the different toolbars on the page after a
* given amount of time has passed since the
* last time the cursor was moved.
*/
waitForHide = idle(() =>
{
this.props.setToolbarsVisible(false);
}, TIMEOUT);
handleMouseMove = () =>
{
// If the toolbars were hidden, show them again when
// the user moves their cursor.
if (!this.props.room.toolbarsVisible)
{
this.props.setToolbarsVisible(true);
}
this.waitForHide();
}
componentDidMount()
{
window.addEventListener('mousemove', this.handleMouseMove);
}
componentWillUnmount()
{
window.removeEventListener('mousemove', this.handleMouseMove);
}
render() render()
{ {
const { const {
@ -77,8 +114,12 @@ class Room extends React.Component
</div> </div>
:null :null
} }
<div className='room-link-wrapper'> <div
className={classnames('room-link-wrapper room-controls', {
'visible' : this.props.room.toolbarsVisible
})}
>
<div className='room-link'> <div className='room-link'>
<ClipboardButton <ClipboardButton
component='a' component='a'
@ -125,7 +166,10 @@ class Room extends React.Component
</div> </div>
</Draggable> </Draggable>
<div className='sidebar'> <div className={classnames('sidebar room-controls', {
'visible' : this.props.room.toolbarsVisible
})}
>
<div <div
className={classnames('button', 'screen', screenState)} className={classnames('button', 'screen', screenState)}
data-tip={screenTip} data-tip={screenTip}
@ -204,17 +248,18 @@ class Room extends React.Component
Room.propTypes = Room.propTypes =
{ {
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
amActiveSpeaker : PropTypes.bool.isRequired, amActiveSpeaker : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired, toolAreaOpen : PropTypes.bool.isRequired,
screenProducer : appPropTypes.Producer, screenProducer : appPropTypes.Producer,
onRoomLinkCopy : PropTypes.func.isRequired, onRoomLinkCopy : PropTypes.func.isRequired,
onShareScreen : PropTypes.func.isRequired, onShareScreen : PropTypes.func.isRequired,
onUnShareScreen : PropTypes.func.isRequired, onUnShareScreen : PropTypes.func.isRequired,
onNeedExtension : PropTypes.func.isRequired, onNeedExtension : PropTypes.func.isRequired,
onLeaveMeeting : PropTypes.func.isRequired, onLeaveMeeting : PropTypes.func.isRequired,
onLogin : PropTypes.func.isRequired onLogin : PropTypes.func.isRequired,
setToolbarsVisible : PropTypes.func.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
@ -261,6 +306,10 @@ const mapDispatchToProps = (dispatch) =>
onLogin : () => onLogin : () =>
{ {
dispatch(requestActions.userLogin()); dispatch(requestActions.userLogin());
},
setToolbarsVisible : (visible) =>
{
dispatch(stateActions.setToolbarsVisible(visible));
} }
}; };
}; };

View File

@ -17,8 +17,9 @@ class ToolAreaButton extends React.Component
return ( return (
<div data-component='ToolAreaButton'> <div data-component='ToolAreaButton'>
<div <div
className={classnames('button', 'toolarea-button', { className={classnames('button toolarea-button room-controls', {
on : toolAreaOpen on : toolAreaOpen,
visible : this.props.visible
})} })}
data-tip='Toggle tool area' data-tip='Toggle tool area'
data-type='dark' data-type='dark'
@ -40,13 +41,15 @@ ToolAreaButton.propTypes =
{ {
toolAreaOpen : PropTypes.bool.isRequired, toolAreaOpen : PropTypes.bool.isRequired,
toggleToolArea : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired,
unread : PropTypes.bool.isRequired unread : PropTypes.bool.isRequired,
visible : PropTypes.bool.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
toolAreaOpen : state.toolarea.toolAreaOpen, toolAreaOpen : state.toolarea.toolAreaOpen,
visible : state.room.toolbarsVisible,
unread : state.toolarea.unread unread : state.toolarea.unread
}; };
}; };

View File

@ -5,7 +5,8 @@ const initialState =
activeSpeakerName : null, activeSpeakerName : null,
showSettings : false, showSettings : false,
advancedMode : false, advancedMode : false,
fullScreenConsumer : null // ConsumerID fullScreenConsumer : null, // ConsumerID
toolbarsVisible : true
}; };
const room = (state = initialState, action) => const room = (state = initialState, action) =>
@ -58,6 +59,12 @@ const room = (state = initialState, action) =>
return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId }; return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId };
} }
case 'SET_TOOLBARS_VISIBLE':
{
const { toolbarsVisible } = action.payload;
return { ...state, toolbarsVisible };
}
default: default:
return state; return state;
} }

View File

@ -369,6 +369,11 @@ export const toggleConsumerFullscreen = (consumerId) =>
}; };
}; };
export const setToolbarsVisible = (toolbarsVisible) => ({
type : 'SET_TOOLBARS_VISIBLE',
payload : { toolbarsVisible }
});
export const increaseBadge = () => export const increaseBadge = () =>
{ {
return { return {

View File

@ -43,3 +43,23 @@ export function getBrowserType()
return 'N/A'; return 'N/A';
} }
/**
* Create a function which will call the callback function
* after the given amount of milliseconds has passed since
* the last time the callback function was called.
*/
export const idle = (callback, delay) =>
{
let handle;
return () =>
{
if (handle)
{
clearTimeout(handle);
}
handle = setTimeout(callback, delay);
};
};

5150
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,65 +8,65 @@
"main": "lib/index.jsx", "main": "lib/index.jsx",
"dependencies": { "dependencies": {
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"classnames": "^2.2.5", "classnames": "^2.2.6",
"debug": "^3.1.0", "debug": "^3.1.0",
"domready": "^1.0.8", "domready": "^1.0.8",
"hark": "^1.1.6", "hark": "^1.2.0",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"marked": "^0.3.17", "marked": "^0.4.0",
"mediasoup-client": "^2.1.1", "mediasoup-client": "^2.1.1",
"prop-types": "^15.6.0", "prop-types": "^15.6.2",
"protoo-client": "^2.0.7", "protoo-client": "^3.0.0",
"random-string": "^0.2.0", "random-string": "^0.2.0",
"react": "^16.2.0", "react": "^16.4.1",
"react-clipboard.js": "^1.1.3", "react-clipboard.js": "^1.1.4",
"react-dom": "^16.2.0", "react-dom": "^16.4.1",
"react-draggable": "^3.0.5", "react-draggable": "^3.0.5",
"react-dropdown": "^1.5.0", "react-dropdown": "^1.5.0",
"react-redux": "^5.0.6", "react-redux": "^5.0.7",
"react-spinner": "^0.2.7", "react-spinner": "^0.2.7",
"react-tooltip": "^3.4.0", "react-tooltip": "^3.6.1",
"react-transition-group": "^2.2.1", "react-transition-group": "^2.4.0",
"redux": "^3.7.2", "redux": "^4.0.0",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.3.0",
"riek": "^1.1.0", "riek": "^1.1.0",
"url-parse": "^1.2.0" "url-parse": "^1.4.1"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.0", "babel-core": "^6.26.3",
"babel-plugin-transform-object-assign": "^6.22.0", "babel-eslint": "^8.2.6",
"babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-env": "^1.7.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1", "babel-preset-react-app": "^3.1.2",
"babel-preset-stage-0": "^6.24.1", "babel-preset-stage-0": "^6.24.1",
"babelify": "^8.0.0", "babelify": "^8.0.0",
"browser-sync": "^2.23.6", "browser-sync": "^2.24.5",
"browserify": "^16.1.0", "browserify": "^16.2.2",
"del": "^3.0.0", "del": "^3.0.0",
"envify": "^4.1.0", "envify": "^4.1.0",
"eslint": "^4.17.0", "eslint": "^5.1.0",
"eslint-plugin-import": "^2.8.0", "eslint-plugin-import": "^2.13.0",
"eslint-plugin-react": "^7.6.1", "eslint-plugin-react": "^7.10.0",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-css-base64": "^1.3.4",
"gulp-eslint": "^4.0.2",
"gulp-change": "^1.0.0", "gulp-change": "^1.0.0",
"gulp-header": "^2.0.1", "gulp-css-base64": "^1.3.4",
"gulp-eslint": "^5.0.0",
"gulp-header": "^2.0.5",
"gulp-if": "^2.0.2", "gulp-if": "^2.0.2",
"gulp-plumber": "^1.2.0", "gulp-plumber": "^1.2.0",
"gulp-rename": "^1.2.2", "gulp-rename": "^1.3.0",
"gulp-stylus": "^2.7.0", "gulp-stylus": "^2.7.0",
"gulp-touch-cmd": "0.0.1", "gulp-touch-cmd": "0.0.1",
"gulp-uglify": "^3.0.0", "gulp-uglify": "^3.0.0",
"gulp-util": "^3.0.8", "gulp-util": "^3.0.8",
"lodash": "^4.17.10",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nib": "^1.1.2", "nib": "^1.1.2",
"supports-color": "^5.2.0", "supports-color": "^5.4.0",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
"watchify": "^3.10.0" "watchify": "^3.11.0"
} }
} }

View File

@ -27,7 +27,16 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
padding: 0.4vmin; padding: 0.4vmin;
visibility: hidden;
opacity: 0;
animation: fade-out 0.3s;
&.visible {
visibility: visible;
opacity: 1;
animation: fade-in 0.3s;
}
> .button { > .button {
flex: 0 0 auto; flex: 0 0 auto;
margin: 0.2vmin; margin: 0.2vmin;

View File

@ -41,7 +41,16 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
padding: 0.4vmin; padding: 0.4vmin;
visibility: hidden;
opacity: 0;
animation: fade-out 0.3s;
&.visible {
visibility: visible;
opacity: 1;
animation: fade-in 0.3s;
}
> .button { > .button {
flex: 0 0 auto; flex: 0 0 auto;
margin: 0.2vmin; margin: 0.2vmin;

View File

@ -276,6 +276,18 @@
} }
} }
.room-controls {
visibility: hidden;
animation: fade-out 0.5s;
opacity: 0;
&.visible {
visibility: visible;
opacity: 1;
animation: fade-in 0.5s;
}
}
.Dropdown-root { .Dropdown-root {
position: relative; position: relative;
padding: 0.3vmin; padding: 0.3vmin;

View File

@ -5,6 +5,7 @@ global-reset();
@import './mixins'; @import './mixins';
@import './fonts'; @import './fonts';
@import './reset'; @import './reset';
@import './keyframes';
html { html {
height: 100%; height: 100%;

View File

@ -0,0 +1,23 @@
@keyframes fade-in {
from {
opacity: 0;
visibility: hidden;
}
to {
opacity: 1;
visibility: visible;
}
}
@keyframes fade-out {
from {
opacity: 1;
visibility: visible;
}
to {
opacity: 0;
visibility: hidden;
}
}