feat(filmstrip): show thumbnails with toolbar and on hover (#1944)
* feat(filmstrip): show thumbnails with toolbar and on hover * squash: reduce verbosity of logic for when to display * squash: remove check for fake participant Before fake participant (youtube video) would make the filmstrip always displayed. However, youtube videos already dock the toolbar, so filmstrip will remain displayed, so the check is redundant. * squash: change mouse hover listener targets
This commit is contained in:
parent
6682543691
commit
bf03e73876
|
@ -1,5 +1,16 @@
|
||||||
/**
|
/**
|
||||||
* The type of action sets the visibility of the entire filmstrip;
|
* The type of the action which sets whether or not the filmstrip is being
|
||||||
|
* hovered with the cursor.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: SET_FILMSTRIP_HOVERED,
|
||||||
|
* hovered: boolean
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const SET_FILMSTRIP_HOVERED = Symbol('SET_FILMSTRIP_HOVERED');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of action sets the visibility of the entire filmstrip.
|
||||||
*
|
*
|
||||||
* {
|
* {
|
||||||
* type: SET_FILMSTRIP_VISIBILITY,
|
* type: SET_FILMSTRIP_VISIBILITY,
|
||||||
|
|
|
@ -1,7 +1,25 @@
|
||||||
import {
|
import {
|
||||||
|
SET_FILMSTRIP_HOVERED,
|
||||||
SET_FILMSTRIP_VISIBILITY
|
SET_FILMSTRIP_VISIBILITY
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets if the filmstrip is currently being hovered over.
|
||||||
|
*
|
||||||
|
* @param {boolean} hovered - Whether or not the filmstrip is currently being
|
||||||
|
* hovered over.
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_FILMSTRIP_HOVERED,
|
||||||
|
* hovered: boolean
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setFilmstripHovered(hovered) {
|
||||||
|
return {
|
||||||
|
type: SET_FILMSTRIP_HOVERED,
|
||||||
|
hovered
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets if the entire filmstrip should be visible.
|
* Sets if the entire filmstrip should be visible.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { Toolbox } from '../../toolbox';
|
import { Toolbox } from '../../toolbox';
|
||||||
|
|
||||||
|
import { setFilmstripHovered } from '../actions';
|
||||||
import { shouldRemoteVideosBeVisible } from '../functions';
|
import { shouldRemoteVideosBeVisible } from '../functions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,19 +16,68 @@ import { shouldRemoteVideosBeVisible } from '../functions';
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
class Filmstrip extends Component {
|
class Filmstrip extends Component {
|
||||||
|
_isHovered: boolean;
|
||||||
|
|
||||||
|
_notifyOfHoveredStateUpdate: Function;
|
||||||
|
|
||||||
|
_onMouseOut: Function;
|
||||||
|
|
||||||
|
_onMouseOver: Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code Filmstrip} component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* Whether or not remote videos are currently being hovered over.
|
||||||
|
*/
|
||||||
|
_hovered: React.PropTypes.bool,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the remote videos should be visible. Will toggle
|
* Whether or not the remote videos should be visible. Will toggle
|
||||||
* a class for hiding the videos.
|
* a class for hiding the videos.
|
||||||
*/
|
*/
|
||||||
_remoteVideosVisible: React.PropTypes.bool,
|
_remoteVideosVisible: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the redux store with filmstrip hover changes.
|
||||||
|
*/
|
||||||
|
dispatch: React.PropTypes.func,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the toolbox should be displayed within the filmstrip.
|
* Whether or not the toolbox should be displayed within the filmstrip.
|
||||||
*/
|
*/
|
||||||
displayToolbox: React.PropTypes.bool
|
displayToolbox: React.PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code Filmstrip} instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only properties with which the new
|
||||||
|
* instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Debounce the method for dispatching the new filmstrip handled state
|
||||||
|
// so that it does not get called with each mouse movement event. This
|
||||||
|
// also works around an issue where mouseout and then a mouseover event
|
||||||
|
// is fired when hovering over remote thumbnails, which are not yet in
|
||||||
|
// react.
|
||||||
|
this._notifyOfHoveredStateUpdate
|
||||||
|
= _.debounce(this._notifyOfHoveredStateUpdate, 100);
|
||||||
|
|
||||||
|
// Cache the current hovered state for _updateHoveredState to always
|
||||||
|
// send the last known hovered state.
|
||||||
|
this._isHovered = false;
|
||||||
|
|
||||||
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
|
this._onMouseOver = this._onMouseOver.bind(this);
|
||||||
|
this._onMouseOut = this._onMouseOut.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -54,7 +105,9 @@ class Filmstrip extends Component {
|
||||||
id = 'remoteVideos'>
|
id = 'remoteVideos'>
|
||||||
<div
|
<div
|
||||||
className = 'filmstrip__videos'
|
className = 'filmstrip__videos'
|
||||||
id = 'filmstripLocalVideo' />
|
id = 'filmstripLocalVideo'
|
||||||
|
onMouseOut = { this._onMouseOut }
|
||||||
|
onMouseOver = { this._onMouseOver } />
|
||||||
<div
|
<div
|
||||||
className = 'filmstrip__videos'
|
className = 'filmstrip__videos'
|
||||||
id = 'filmstripRemoteVideos'>
|
id = 'filmstripRemoteVideos'>
|
||||||
|
@ -65,7 +118,9 @@ class Filmstrip extends Component {
|
||||||
*/}
|
*/}
|
||||||
<div
|
<div
|
||||||
className = 'remote-videos-container'
|
className = 'remote-videos-container'
|
||||||
id = 'filmstripRemoteVideosContainer' />
|
id = 'filmstripRemoteVideosContainer'
|
||||||
|
onMouseOut = { this._onMouseOut }
|
||||||
|
onMouseOver = { this._onMouseOver } />
|
||||||
</div>
|
</div>
|
||||||
<audio
|
<audio
|
||||||
id = 'userJoined'
|
id = 'userJoined'
|
||||||
|
@ -79,6 +134,43 @@ class Filmstrip extends Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the current hover state does not match the known hover state in redux,
|
||||||
|
* dispatch an action to update the known hover state in redux.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_notifyOfHoveredStateUpdate() {
|
||||||
|
if (this.props._hovered !== this._isHovered) {
|
||||||
|
this.props.dispatch(setFilmstripHovered(this._isHovered));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the currently known mouseover state and attempt to dispatch an
|
||||||
|
* update of the known hover state in redux.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onMouseOut() {
|
||||||
|
this._isHovered = false;
|
||||||
|
this._notifyOfHoveredStateUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the currently known mouseover state and attempt to dispatch an
|
||||||
|
* update of the known hover state in redux.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onMouseOver() {
|
||||||
|
this._isHovered = true;
|
||||||
|
this._notifyOfHoveredStateUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -87,11 +179,13 @@ class Filmstrip extends Component {
|
||||||
* @param {Object} state - The Redux state.
|
* @param {Object} state - The Redux state.
|
||||||
* @private
|
* @private
|
||||||
* @returns {{
|
* @returns {{
|
||||||
|
* _hovered: boolean,
|
||||||
* _remoteVideosVisible: boolean
|
* _remoteVideosVisible: boolean
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
|
_hovered: state['features/filmstrip'].hovered,
|
||||||
_remoteVideosVisible: shouldRemoteVideosBeVisible(state)
|
_remoteVideosVisible: shouldRemoteVideosBeVisible(state)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,21 +14,22 @@ import {
|
||||||
*/
|
*/
|
||||||
export function shouldRemoteVideosBeVisible(state) {
|
export function shouldRemoteVideosBeVisible(state) {
|
||||||
const participants = state['features/base/participants'];
|
const participants = state['features/base/participants'];
|
||||||
|
const participantsCount = participants.length;
|
||||||
|
|
||||||
const shouldShowVideos
|
const shouldShowVideos
|
||||||
= state['features/base/config'].disable1On1Mode
|
= participantsCount > 2
|
||||||
|
|
||||||
|
// Always show the filmstrip when there is another participant to show
|
||||||
|
// and the filmstrip is hovered, or local video is pinned, or the
|
||||||
|
// toolbar is displayed.
|
||||||
|
|| (participantsCount > 1
|
||||||
|
&& (state['features/filmstrip'].hovered
|
||||||
|
|| state['features/toolbox'].visible
|
||||||
|
|| getLocalParticipant(state) === getPinnedParticipant(state)))
|
||||||
|
|
||||||
|| interfaceConfig.filmStripOnly
|
|| interfaceConfig.filmStripOnly
|
||||||
|
|
||||||
// This is not a 1-on-1 call.
|
|| state['features/base/config'].disable1On1Mode;
|
||||||
|| participants.length > 2
|
|
||||||
|
|
||||||
// There is another participant and the local participant is pinned.
|
|
||||||
|| (participants.length > 1
|
|
||||||
&& getLocalParticipant(state) === getPinnedParticipant(state))
|
|
||||||
|
|
||||||
// There is any non-person participant, like a shared video.
|
|
||||||
|| participants.find(participant => participant.isBot);
|
|
||||||
|
|
||||||
return Boolean(shouldShowVideos);
|
return Boolean(shouldShowVideos);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ReducerRegistry } from '../base/redux';
|
import { ReducerRegistry } from '../base/redux';
|
||||||
import {
|
import {
|
||||||
|
SET_FILMSTRIP_HOVERED,
|
||||||
SET_FILMSTRIP_VISIBILITY
|
SET_FILMSTRIP_VISIBILITY
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
|
||||||
|
@ -11,6 +12,12 @@ ReducerRegistry.register(
|
||||||
'features/filmstrip',
|
'features/filmstrip',
|
||||||
(state = DEFAULT_STATE, action) => {
|
(state = DEFAULT_STATE, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case SET_FILMSTRIP_HOVERED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
hovered: action.hovered
|
||||||
|
};
|
||||||
|
|
||||||
case SET_FILMSTRIP_VISIBILITY:
|
case SET_FILMSTRIP_VISIBILITY:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
Loading…
Reference in New Issue