{
* @param {Object} state - The Redux state.
* @private
* @returns {{
- * _filmstripVisible: boolean
+ * _filmstripVisible: boolean,
+ * _showVideoQualityLabel: boolean
* }}
*/
export function _abstractMapStateToProps(state: Object) {
return {
- _filmstripVisible: isFilmstripVisible(state)
+ _filmstripVisible: isFilmstripVisible(state),
+ _showVideoQualityLabel: !shouldDisplayTileView(state)
};
}
diff --git a/react/features/large-video/components/Labels.web.js b/react/features/large-video/components/Labels.web.js
index c5e5c5343..41df7a7cb 100644
--- a/react/features/large-video/components/Labels.web.js
+++ b/react/features/large-video/components/Labels.web.js
@@ -89,7 +89,8 @@ class Labels extends AbstractLabels
{ + accessibilityLabel = 'toolbar.accessibilityLabel.tileView'; + iconName = 'icon-tiles-many'; + toggledIconName = 'icon-tiles-many toggled'; + tooltip = 'toolbar.tileViewToggle'; + + /** + * Handles clicking / pressing the button. + * + * @override + * @protected + * @returns {void} + */ + _handleClick() { + const { _tileViewEnabled, dispatch } = this.props; + + sendAnalytics(createToolbarEvent( + 'tileview.button', + { + 'is_enabled': _tileViewEnabled + })); + + dispatch(setTileView(!_tileViewEnabled)); + } + + /** + * Indicates whether this button is in toggled state or not. + * + * @override + * @protected + * @returns {boolean} + */ + _isToggled() { + return this.props._tileViewEnabled; + } +} + +/** + * Maps (parts of) the redux state to the associated props for the + * {@code TileViewButton} component. + * + * @param {Object} state - The Redux state. + * @returns {{ + * _tileViewEnabled: boolean + * }} + */ +function _mapStateToProps(state) { + return { + _tileViewEnabled: state['features/video-layout'].tileViewEnabled + }; +} + +export default translate(connect(_mapStateToProps)(TileViewButton)); diff --git a/react/features/video-layout/components/index.js b/react/features/video-layout/components/index.js new file mode 100644 index 000000000..baaeb408c --- /dev/null +++ b/react/features/video-layout/components/index.js @@ -0,0 +1 @@ +export { default as TileViewButton } from './TileViewButton'; diff --git a/react/features/video-layout/constants.js b/react/features/video-layout/constants.js new file mode 100644 index 000000000..72488205f --- /dev/null +++ b/react/features/video-layout/constants.js @@ -0,0 +1,10 @@ +/** + * An enumeration of the different display layouts supported by the application. + * + * @type {Object} + */ +export const LAYOUTS = { + HORIZONTAL_FILMSTRIP_VIEW: 'horizontal-filmstrip-view', + TILE_VIEW: 'tile-view', + VERTICAL_FILMSTRIP_VIEW: 'vertical-filmstrip-view' +}; diff --git a/react/features/video-layout/functions.js b/react/features/video-layout/functions.js new file mode 100644 index 000000000..31cdf97ed --- /dev/null +++ b/react/features/video-layout/functions.js @@ -0,0 +1,78 @@ +// @flow + +import { LAYOUTS } from './constants'; + +declare var interfaceConfig: Object; + +/** + * Returns the {@code LAYOUTS} constant associated with the layout + * the application should currently be in. + * + * @param {Object} state - The redux state. + * @returns {string} + */ +export function getCurrentLayout(state: Object) { + if (shouldDisplayTileView(state)) { + return LAYOUTS.TILE_VIEW; + } else if (interfaceConfig.VERTICAL_FILMSTRIP) { + return LAYOUTS.VERTICAL_FILMSTRIP_VIEW; + } + + return LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW; +} + +/** + * Returns how many columns should be displayed in tile view. The number + * returned will be between 1 and 5, inclusive. + * + * @returns {number} + */ +export function getMaxColumnCount() { + const configuredMax = interfaceConfig.TILE_VIEW_MAX_COLUMNS || 5; + + return Math.max(Math.min(configuredMax, 1), 5); +} + +/** + * Returns the cell count dimensions for tile view. Tile view tries to uphold + * equal count of tiles for height and width, until maxColumn is reached in + * which rows will be added but no more columns. + * + * @param {Object} state - The redux state. + * @param {number} maxColumns - The maximum number of columns that can be + * displayed. + * @returns {Object} An object is return with the desired number of columns, + * rows, and visible rows (the rest should overflow) for the tile view layout. + */ +export function getTileViewGridDimensions(state: Object, maxColumns: number) { + // Purposefully include all participants, which includes fake participants + // that should show a thumbnail. + const potentialThumbnails = state['features/base/participants'].length; + + const columnsToMaintainASquare = Math.ceil(Math.sqrt(potentialThumbnails)); + const columns = Math.min(columnsToMaintainASquare, maxColumns); + const rows = Math.ceil(potentialThumbnails / columns); + const visibleRows = Math.min(maxColumns, rows); + + return { + columns, + rows, + visibleRows + }; +} + +/** + * Selector for determining if the UI layout should be in tile view. Tile view + * is determined by more than just having the tile view setting enabled, as + * one-on-one calls should not be in tile view, as well as etherpad editing. + * + * @param {Object} state - The redux state. + * @returns {boolean} True if tile view should be displayed. + */ +export function shouldDisplayTileView(state: Object = {}) { + return Boolean( + state['features/video-layout'] + && state['features/video-layout'].tileViewEnabled + && !state['features/etherpad'].editing + ); +} diff --git a/react/features/video-layout/index.js b/react/features/video-layout/index.js index d43689289..a1178fd2b 100644 --- a/react/features/video-layout/index.js +++ b/react/features/video-layout/index.js @@ -1 +1,9 @@ +export * from './actions'; +export * from './actionTypes'; +export * from './components'; +export * from './constants'; +export * from './functions'; + import './middleware'; +import './reducer'; +import './subscriber'; diff --git a/react/features/video-layout/middleware.web.js b/react/features/video-layout/middleware.web.js index 35eb1f305..a3cce4527 100644 --- a/react/features/video-layout/middleware.web.js +++ b/react/features/video-layout/middleware.web.js @@ -15,6 +15,8 @@ import { import { MiddlewareRegistry } from '../base/redux'; import { TRACK_ADDED } from '../base/tracks'; +import { SET_TILE_VIEW } from './actionTypes'; + declare var APP: Object; /** @@ -71,6 +73,10 @@ MiddlewareRegistry.register(store => next => action => { Boolean(action.participant.id)); break; + case SET_TILE_VIEW: + APP.UI.emitEvent(UIEvents.TOGGLED_TILE_VIEW, action.enabled); + break; + case TRACK_ADDED: if (!action.track.local) { VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack); diff --git a/react/features/video-layout/reducer.js b/react/features/video-layout/reducer.js new file mode 100644 index 000000000..dc99f4018 --- /dev/null +++ b/react/features/video-layout/reducer.js @@ -0,0 +1,17 @@ +// @flow + +import { ReducerRegistry } from '../base/redux'; + +import { SET_TILE_VIEW } from './actionTypes'; + +ReducerRegistry.register('features/video-layout', (state = {}, action) => { + switch (action.type) { + case SET_TILE_VIEW: + return { + ...state, + tileViewEnabled: action.enabled + }; + } + + return state; +}); diff --git a/react/features/video-layout/subscriber.js b/react/features/video-layout/subscriber.js new file mode 100644 index 000000000..01c408f6c --- /dev/null +++ b/react/features/video-layout/subscriber.js @@ -0,0 +1,24 @@ +// @flow + +import { + VIDEO_QUALITY_LEVELS, + setMaxReceiverVideoQuality +} from '../base/conference'; +import { StateListenerRegistry } from '../base/redux'; +import { selectParticipant } from '../large-video'; +import { shouldDisplayTileView } from './functions'; + +/** + * StateListenerRegistry provides a reliable way of detecting changes to + * preferred layout state and dispatching additional actions. + */ +StateListenerRegistry.register( + /* selector */ state => shouldDisplayTileView(state), + /* listener */ (displayTileView, { dispatch }) => { + dispatch(selectParticipant()); + + if (!displayTileView) { + dispatch(setMaxReceiverVideoQuality(VIDEO_QUALITY_LEVELS.HIGH)); + } + } +); diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index b23ff21a7..65cb48ac5 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -59,6 +59,7 @@ export default { TOGGLED_FILMSTRIP: 'UI.toggled_filmstrip', TOGGLE_SCREENSHARING: 'UI.toggle_screensharing', TOGGLED_SHARED_DOCUMENT: 'UI.toggled_shared_document', + TOGGLED_TILE_VIEW: 'UI.toggled_tile_view', HANGUP: 'UI.hangup', LOGOUT: 'UI.logout', VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',