diff --git a/app/lib/components/Filmstrip.jsx b/app/lib/components/Filmstrip.jsx new file mode 100644 index 0000000..54a46e1 --- /dev/null +++ b/app/lib/components/Filmstrip.jsx @@ -0,0 +1,172 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ResizeObserver from 'resize-observer-polyfill'; +import { connect } from 'react-redux'; +import classnames from 'classnames'; +import Peer from './Peer'; + +class Filmstrip extends Component +{ + constructor(props) + { + super(props); + + this.activePeerContainer = React.createRef(); + } + + state = { + selectedPeerName : null, + lastSpeaker : null, + width : 400 + }; + + // Find the name of the peer which is currently speaking. This is either + // the latest active speaker, or the manually selected peer. + getActivePeerName = () => + this.state.selectedPeerName || this.state.lastSpeaker; + + isSharingCamera = (peerName) => this.props.peers[peerName] && + this.props.peers[peerName].consumers.some((consumer) => + this.props.consumers[consumer].source === 'screen'); + + getRatio = () => + { + let ratio = 4 / 3; + + if (this.isSharingCamera(this.getActivePeerName())) + { + ratio *= 2; + } + + return ratio; + }; + + updateDimensions = () => + { + const container = this.activePeerContainer.current; + + if (container) + { + const ratio = this.getRatio(); + + let width = container.clientWidth; + + if (width / ratio > container.clientHeight) + { + width = container.clientHeight * ratio; + } + + this.setState({ + width + }); + } + }; + + componentDidMount() + { + window.addEventListener('resize', this.updateDimensions); + const observer = new ResizeObserver(this.updateDimensions); + + observer.observe(this.activePeerContainer.current); + this.updateDimensions(); + } + + componentWillUnmount() + { + window.removeEventListener('resize', this.updateDimensions); + } + + componentDidUpdate(prevProps) + { + if (prevProps !== this.props) + { + this.updateDimensions(); + + if (this.props.activeSpeakerName !== this.props.myName) + { + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + lastSpeaker : this.props.activeSpeakerName + }); + } + } + } + + handleSelectPeer = (selectedPeerName) => + { + this.setState((oldState) => ({ + selectedPeerName : oldState.selectedPeerName === selectedPeerName ? + null : selectedPeerName + })); + }; + + render() + { + const { peers, advancedMode } = this.props; + + const activePeerName = this.getActivePeerName(); + + return ( +
+
+ {peers[activePeerName] && ( +
+ +
+ )} +
+ +
+
+ {Object.keys(peers).map((peerName) => ( +
this.handleSelectPeer(peerName)} + className={classnames('film', { + selected : this.state.selectedPeerName === peerName, + active : this.state.lastSpeaker === peerName + })} + > +
+ +
+
+ ))} +
+
+
+ ); + } +} + +Filmstrip.propTypes = { + activeSpeakerName : PropTypes.string, + advancedMode : PropTypes.bool, + peers : PropTypes.object.isRequired, + consumers : PropTypes.object.isRequired, + myName : PropTypes.string.isRequired +}; + +const mapStateToProps = (state) => + ({ + activeSpeakerName : state.room.activeSpeakerName, + peers : state.peers, + consumers : state.consumers, + myName : state.me.name + }); + +export default connect( + mapStateToProps +)(Filmstrip); diff --git a/app/lib/components/Peers.jsx b/app/lib/components/Peers.jsx index fccebb8..de8cbbc 100644 --- a/app/lib/components/Peers.jsx +++ b/app/lib/components/Peers.jsx @@ -14,14 +14,22 @@ class Peers extends React.Component constructor(props) { super(props); + this.state = { peerWidth : 400, peerHeight : 300 }; + + this.peersRef = React.createRef(); } updateDimensions = () => { + if (!this.peersRef.current) + { + return; + } + const n = this.props.boxes; if (n === 0) @@ -29,8 +37,8 @@ class Peers extends React.Component return; } - const width = this.refs.peers.clientWidth; - const height = this.refs.peers.clientHeight; + const width = this.peersRef.current.clientWidth; + const height = this.peersRef.current.clientHeight; let x, y, space; @@ -64,7 +72,7 @@ class Peers extends React.Component window.addEventListener('resize', this.updateDimensions); const observer = new ResizeObserver(this.updateDimensions); - observer.observe(this.refs.peers); + observer.observe(this.peersRef.current); } componentWillUnmount() @@ -82,8 +90,7 @@ class Peers extends React.Component const { advancedMode, activeSpeakerName, - peers, - toolAreaOpen + peers } = this.props; const style = @@ -93,7 +100,7 @@ class Peers extends React.Component }; return ( -
+
{ peers.map((peer) => { @@ -108,7 +115,6 @@ class Peers extends React.Component advancedMode={advancedMode} name={peer.name} style={style} - toolAreaOpen={toolAreaOpen} />
@@ -125,8 +131,7 @@ Peers.propTypes = advancedMode : PropTypes.bool, peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, boxes : PropTypes.number, - activeSpeakerName : PropTypes.string, - toolAreaOpen : PropTypes.bool + activeSpeakerName : PropTypes.string }; const mapStateToProps = (state) => @@ -139,8 +144,7 @@ const mapStateToProps = (state) => return { peers, boxes, - activeSpeakerName : state.room.activeSpeakerName, - toolAreaOpen : state.toolarea.toolAreaOpen + activeSpeakerName : state.room.activeSpeakerName }; }; diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index 7202c90..6aeb26e 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -17,6 +17,7 @@ import FullScreenView from './FullScreenView'; import Draggable from 'react-draggable'; import { idle } from '../utils'; import Sidebar from './Sidebar'; +import Filmstrip from './Filmstrip'; // Hide toolbars after 10 seconds of inactivity. const TIMEOUT = 10 * 1000; @@ -64,6 +65,11 @@ class Room extends React.Component onRoomLinkCopy } = this.props; + const View = { + filmstrip : Filmstrip, + democratic : Peers + }[room.mode]; + return (
@@ -121,9 +127,7 @@ class Room extends React.Component
- +
{ - super(props); + this.props.handleChangeWebcam(webcam.value); + + this.setState({ + selectedCamera : webcam + }); } - render() + handleChangeAudioDevice = (device) => { + this.props.handleChangeAudioDevice(device.value); + + this.setState({ + selectedAudioDevice : device + }); + } + + handleChangeMode = (mode) => + { + this.setState({ + selectedMode : mode + }); + + this.props.handleChangeMode(mode.value); + }; + + render() + { const { room, me, - handleChangeWebcam, - handleChangeAudioDevice, onToggleAdvancedMode } = this.props; @@ -55,21 +89,32 @@ class Settings extends React.Component + + - Advanced mode + + +
); @@ -82,7 +127,8 @@ Settings.propTypes = room : appPropTypes.Room.isRequired, handleChangeWebcam : PropTypes.func.isRequired, handleChangeAudioDevice : PropTypes.func.isRequired, - onToggleAdvancedMode : PropTypes.func.isRequired + onToggleAdvancedMode : PropTypes.func.isRequired, + handleChangeMode : PropTypes.func.isRequired }; const mapStateToProps = (state) => @@ -93,22 +139,11 @@ const mapStateToProps = (state) => }; }; -const mapDispatchToProps = (dispatch) => -{ - return { - handleChangeWebcam : (device) => - { - dispatch(requestActions.changeWebcam(device.value)); - }, - handleChangeAudioDevice : (device) => - { - dispatch(requestActions.changeAudioDevice(device.value)); - }, - onToggleAdvancedMode : () => - { - dispatch(stateActions.toggleAdvancedMode()); - } - }; +const mapDispatchToProps = { + handleChangeWebcam : requestActions.changeWebcam, + handleChangeAudioDevice : requestActions.changeAudioDevice, + onToggleAdvancedMode : stateActions.toggleAdvancedMode, + handleChangeMode : stateActions.setDisplayMode }; const SettingsContainer = connect( diff --git a/app/lib/redux/reducers/room.js b/app/lib/redux/reducers/room.js index e92196b..4a787de 100644 --- a/app/lib/redux/reducers/room.js +++ b/app/lib/redux/reducers/room.js @@ -6,7 +6,8 @@ const initialState = showSettings : false, advancedMode : false, fullScreenConsumer : null, // ConsumerID - toolbarsVisible : true + toolbarsVisible : true, + mode : 'democratic' }; const room = (state = initialState, action) => @@ -65,6 +66,10 @@ const room = (state = initialState, action) => return { ...state, toolbarsVisible }; } + + case 'SET_DISPLAY_MODE': + return { ...state, mode: action.payload.mode }; + default: return state; } diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 9dcbb41..2c51b02 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -93,6 +93,12 @@ export const toggleAdvancedMode = () => }; }; +export const setDisplayMode = (mode) => + ({ + type : 'SET_DISPLAY_MODE', + payload : { mode } + }); + export const setAudioOnlyState = (enabled) => { return { diff --git a/app/stylus/components/Filmstrip.styl b/app/stylus/components/Filmstrip.styl new file mode 100644 index 0000000..65b94ef --- /dev/null +++ b/app/stylus/components/Filmstrip.styl @@ -0,0 +1,78 @@ +[data-component='Filmstrip'] { + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + + > .active-peer-container { + width: 100%; + height: 80vh; + display: flex; + justify-content: center; + align-items: center; + + > .active-peer { + width: 100%; + padding: 1vmin; + + > [data-component='Peer'] { + border: 5px solid rgba(255, 255, 255, 0.15); + box-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5); + } + } + } + + > .filmstrip { + display: flex; + background: rgba(0, 0, 0 , 0.5); + width: 100%; + overflow-x: auto; + height: 20vh; + align-items: center; + + > .filmstrip-content { + margin: 0 auto; + display: flex; + height: 100%; + align-items: center; + + > .film { + height: 18vh; + flex-shrink: 0; + padding-left: 1vh; + + &:last-child { + padding-right: 1vh; + } + + > .film-content { + height: 100%; + width: 100%; + border: 1px solid rgba(255,255,255,0.15); + + > [data-component='Peer'] { + max-width: 18vh * (4 / 3); + cursor: pointer; + + &.screen { + max-width: 18vh * (2 * 4 / 3); + border: 0; + } + } + } + + &.active { + > .film-content { + border-color: #FFF; + } + } + + &.selected { + > .film-content { + border-color: #377EFF; + } + } + } + } + } +} \ No newline at end of file diff --git a/app/stylus/components/Peer.styl b/app/stylus/components/Peer.styl index 5a9e762..7d26715 100644 --- a/app/stylus/components/Peer.styl +++ b/app/stylus/components/Peer.styl @@ -22,13 +22,15 @@ > .view-container { position: relative; - + flex-grow: 1; + &.webcam { order: 2; } &.screen { order: 1; + max-width: 50%; } > .controls { diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl index 55dbe71..dcc62d5 100644 --- a/app/stylus/components/Room.styl +++ b/app/stylus/components/Room.styl @@ -153,13 +153,13 @@ } +desktop() { - bottom: 20px; + top: 20px; left: 20px; border: 1px solid rgba(#fff, 0.15); } +mobile() { - bottom: 10px; + top: 10px; left: 10px; border: 1px solid rgba(#fff, 0.25); } diff --git a/app/stylus/index.styl b/app/stylus/index.styl index 94e08b8..5e67e46 100644 --- a/app/stylus/index.styl +++ b/app/stylus/index.styl @@ -50,6 +50,7 @@ body { @import './components/ParticipantList'; @import './components/FullScreenView'; @import './components/FullView'; + @import './components/Filmstrip'; } // Hack to detect in JS the current media query