jiti-meet/react/features/filmstrip/components/web/Filmstrip.js

397 lines
12 KiB
JavaScript
Raw Normal View History

/* @flow */
import _ from 'lodash';
import React, { Component } from 'react';
2019-03-19 15:42:25 +00:00
import type { Dispatch } from 'redux';
import {
createShortcutEvent,
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
2020-05-20 10:57:03 +00:00
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
2019-03-21 16:38:29 +00:00
import { connect } from '../../../base/redux';
import { dockToolbox } from '../../../toolbox';
2020-05-20 10:57:03 +00:00
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { setFilmstripHovered, setFilmstripVisible } from '../../actions';
import { shouldRemoteVideosBeVisible } from '../../functions';
import Toolbar from './Toolbar';
declare var APP: Object;
declare var interfaceConfig: Object;
2018-06-25 15:33:08 +00:00
/**
* The type of the React {@code Component} props of {@link Filmstrip}.
*/
type Props = {
/**
* Additional CSS class names top add to the root.
*/
_className: string,
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* The number of columns in tile view.
*/
_columns: number,
2018-06-25 15:33:08 +00:00
/**
* Whether the UI/UX is filmstrip-only.
*/
_filmstripOnly: boolean,
/**
* The width of the filmstrip.
*/
_filmstripWidth: number,
/**
* Whether the filmstrip scrollbar should be hidden or not.
*/
_hideScrollbar: boolean,
/**
* Whether the filmstrip toolbar should be hidden or not.
*/
_hideToolbar: boolean,
2018-06-25 15:33:08 +00:00
/**
* Whether or not remote videos are currently being hovered over. Hover
* handling is currently being handled detected outside of react.
*/
_hovered: boolean,
/**
* The number of rows in tile view.
*/
_rows: number,
/**
* Additional CSS class names to add to the container of all the thumbnails.
*/
_videosClassName: string,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_visible: boolean,
2018-06-25 15:33:08 +00:00
/**
* The redux {@code dispatch} function.
*/
dispatch: Dispatch<any>,
/**
* Invoked to obtain translated strings.
*/
t: Function
2018-06-25 15:33:08 +00:00
};
/**
* Implements a React {@link Component} which represents the filmstrip on
* Web/React.
*
* @extends Component
*/
2018-06-25 15:33:08 +00:00
class Filmstrip extends Component <Props> {
_isHovered: boolean;
_notifyOfHoveredStateUpdate: Function;
_onMouseOut: Function;
_onMouseOver: Function;
/**
* Initializes a new {@code Filmstrip} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
2018-06-25 15:33:08 +00:00
constructor(props: 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._onMouseOut = this._onMouseOut.bind(this);
2018-05-03 17:36:29 +00:00
this._onMouseOver = this._onMouseOver.bind(this);
this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
if (!this.props._filmstripOnly) {
APP.keyboardshortcut.registerShortcut(
'F',
'filmstripPopover',
this._onShortcutToggleFilmstrip,
'keyboardShortcuts.toggleFilmstrip'
);
}
}
/**
* Implements React's {@link Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentWillUnmount() {
APP.keyboardshortcut.unregisterShortcut('F');
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
2018-05-03 17:36:29 +00:00
// Note: Appending of {@code RemoteVideo} views is handled through
// VideoLayout. The views do not get blown away on render() because
// ReactDOMComponent is only aware of the given JSX and not new appended
// DOM. As such, when updateDOMProperties gets called, only attributes
// will get updated without replacing the DOM. If the known DOM gets
// modified, then the views will get blown away.
2020-02-07 15:10:52 +00:00
const filmstripStyle = { };
const filmstripRemoteVideosContainerStyle = {};
let remoteVideoContainerClassName = 'remote-videos-container';
switch (this.props._currentLayout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
2020-02-07 15:10:52 +00:00
// Adding 18px for the 2px margins, 2px borders on the left and right and 5px padding on the left and right.
// Also adding 7px for the scrollbar.
filmstripStyle.maxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120) + 25;
break;
case LAYOUTS.TILE_VIEW: {
// The size of the side margins for each tile as set in CSS.
const { _columns, _rows, _filmstripWidth } = this.props;
if (_rows > _columns) {
remoteVideoContainerClassName += ' has-overflow';
}
filmstripRemoteVideosContainerStyle.width = _filmstripWidth;
break;
}
}
let remoteVideosWrapperClassName = 'filmstrip__videos';
if (this.props._hideScrollbar) {
remoteVideosWrapperClassName += ' hide-scrollbar';
}
let toolbar = null;
if (!this.props._hideToolbar) {
toolbar = this.props._filmstripOnly ? <Toolbar /> : this._renderToggleButton();
}
return (
2020-02-07 15:10:52 +00:00
<div
className = { `filmstrip ${this.props._className}` }
style = { filmstripStyle }>
{ toolbar }
<div
className = { this.props._videosClassName }
2020-02-07 15:10:52 +00:00
id = 'remoteVideos'>
<div
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'
onMouseOut = { this._onMouseOut }
WiP(invite-ui): Initial move of invite UI to invite button (#1950) * WiP(invite-ui): Initial move of invite UI to invite button * Adjusts styling to fit both horizontal and vertical filmstrip * Removes comment and functions not needed * [squash] Addressing various review comments * [squash] Move invite options to a separate config * [squash] Adjust invite button styles until we fix the whole UI theme * [squash] Fix the remote videos scroll * [squash]:Do not show popup menu when 1 option is available * [squash]: Disable the invite button in filmstrip mode * feat(connection-indicator): implement automatic hiding on good connection (#2009) * ref(connection-stats): use PropTypes package * feat(connection-stats): display a summary of the connection quality * feat(connection-indicator): show empty bars for interrupted connection * feat(connection-indicator): change background color based on status * feat(connection-indicator): implement automatic hiding on good connection * fix(connection-indicator): explicitly set font size Currently non-react code will set an icon size on ConnectionIndicator. This doesn't work on initial call join in vertical filmstrip after some changes to support hiding the indicator. The chosen fix is passing in the icon size to mirror what would happe with full filmstrip reactification. * ref(connection-stats): rename statuses * feat(connection-indicator): make hiding behavior configurable The original implementation made the auto hiding of the indicator configured in interfaceConfig. * fix(connection-indicator): readd class expected by torture tests * fix(connection-indicator): change connection quality display styling Bold the connection summary in the stats popover so it stands out. Change the summaries so there are only three--strong, nonoptimal, poor. * fix(connection-indicator): gray background on lost connection * feat(icons): add new gsm bars icon * feat(connection-indicator): use new 3-bar icon * ref(icons): remove icon-connection and icon-connection-lost Both have been replaced by icon-gsm-bars so they are not being referenced anymore. Mobile looks to have connect-lost as a separate icon in font-icons/jitsi.json. * fix(defaultToolbarButtons): Fixes unresolved InfoDialogButton component problem * [squash]: Makes invite button fit the container * [squash]:Addressing invite truncate, remote menu position and comment * [squash]:Fix z-index in horizontal mode, z-index in lonely call * [squash]: Fix filmstripOnly property, remove important from css
2017-10-03 16:30:42 +00:00
onMouseOver = { this._onMouseOver }>
<div id = 'filmstripLocalVideoThumbnail' />
WiP(invite-ui): Initial move of invite UI to invite button (#1950) * WiP(invite-ui): Initial move of invite UI to invite button * Adjusts styling to fit both horizontal and vertical filmstrip * Removes comment and functions not needed * [squash] Addressing various review comments * [squash] Move invite options to a separate config * [squash] Adjust invite button styles until we fix the whole UI theme * [squash] Fix the remote videos scroll * [squash]:Do not show popup menu when 1 option is available * [squash]: Disable the invite button in filmstrip mode * feat(connection-indicator): implement automatic hiding on good connection (#2009) * ref(connection-stats): use PropTypes package * feat(connection-stats): display a summary of the connection quality * feat(connection-indicator): show empty bars for interrupted connection * feat(connection-indicator): change background color based on status * feat(connection-indicator): implement automatic hiding on good connection * fix(connection-indicator): explicitly set font size Currently non-react code will set an icon size on ConnectionIndicator. This doesn't work on initial call join in vertical filmstrip after some changes to support hiding the indicator. The chosen fix is passing in the icon size to mirror what would happe with full filmstrip reactification. * ref(connection-stats): rename statuses * feat(connection-indicator): make hiding behavior configurable The original implementation made the auto hiding of the indicator configured in interfaceConfig. * fix(connection-indicator): readd class expected by torture tests * fix(connection-indicator): change connection quality display styling Bold the connection summary in the stats popover so it stands out. Change the summaries so there are only three--strong, nonoptimal, poor. * fix(connection-indicator): gray background on lost connection * feat(icons): add new gsm bars icon * feat(connection-indicator): use new 3-bar icon * ref(icons): remove icon-connection and icon-connection-lost Both have been replaced by icon-gsm-bars so they are not being referenced anymore. Mobile looks to have connect-lost as a separate icon in font-icons/jitsi.json. * fix(defaultToolbarButtons): Fixes unresolved InfoDialogButton component problem * [squash]: Makes invite button fit the container * [squash]:Addressing invite truncate, remote menu position and comment * [squash]:Fix z-index in horizontal mode, z-index in lonely call * [squash]: Fix filmstripOnly property, remove important from css
2017-10-03 16:30:42 +00:00
</div>
<div
className = { remoteVideosWrapperClassName }
id = 'filmstripRemoteVideos'>
2018-05-03 17:36:29 +00:00
{/*
* XXX This extra video container is needed for
* scrolling thumbnails in Firefox; otherwise, the flex
* thumbnails resize instead of causing overflow.
*/}
<div
className = { remoteVideoContainerClassName }
id = 'filmstripRemoteVideosContainer'
onMouseOut = { this._onMouseOut }
onMouseOver = { this._onMouseOver }
style = { filmstripRemoteVideosContainerStyle }>
<div id = 'localVideoTileViewContainer' />
</div>
</div>
</div>
</div>
);
}
/**
* Dispatches an action to change the visibility of the filmstrip.
*
* @private
* @returns {void}
*/
_doToggleFilmstrip() {
this.props.dispatch(setFilmstripVisible(!this.props._visible));
}
/**
* 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(dockToolbox(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();
}
_onShortcutToggleFilmstrip: () => void;
/**
* Creates an analytics keyboard shortcut event and dispatches an action for
* toggling filmstrip visibility.
*
* @private
* @returns {void}
*/
_onShortcutToggleFilmstrip() {
sendAnalytics(createShortcutEvent(
'toggle.filmstrip',
{
enable: this.props._visible
}));
this._doToggleFilmstrip();
}
_onToolbarToggleFilmstrip: () => void;
/**
* Creates an analytics toolbar event and dispatches an action for opening
* the speaker stats modal.
*
* @private
* @returns {void}
*/
_onToolbarToggleFilmstrip() {
sendAnalytics(createToolbarEvent(
'toggle.filmstrip.button',
{
enable: this.props._visible
}));
this._doToggleFilmstrip();
}
/**
* Creates a React Element for changing the visibility of the filmstrip when
* clicked.
*
* @private
* @returns {ReactElement}
*/
_renderToggleButton() {
2019-08-30 16:39:06 +00:00
const icon = this.props._visible ? IconMenuDown : IconMenuUp;
const { t } = this.props;
return (
<div className = 'filmstrip__toolbar'>
<button
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
id = 'toggleFilmstripButton'
onClick = { this._onToolbarToggleFilmstrip }>
2019-08-30 16:39:06 +00:00
<Icon src = { icon } />
</button>
</div>
);
}
}
/**
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
const { iAmSipGateway } = state['features/base/config'];
const { hovered, visible } = state['features/filmstrip'];
2018-06-25 15:33:08 +00:00
const isFilmstripOnly = Boolean(interfaceConfig.filmStripOnly);
const reduceHeight
= !isFilmstripOnly && state['features/toolbox'].visible && interfaceConfig.TOOLBAR_BUTTONS.length;
2018-06-25 15:33:08 +00:00
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${reduceHeight ? 'reduce-height' : ''}`.trim();
const videosClassName = `filmstrip__videos${
isFilmstripOnly ? ' filmstrip__videos-filmstripOnly' : ''}${
visible ? '' : ' hidden'}`;
const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
return {
2018-06-25 15:33:08 +00:00
_className: className,
_columns: gridDimensions.columns,
_currentLayout: getCurrentLayout(state),
2018-06-25 15:33:08 +00:00
_filmstripOnly: isFilmstripOnly,
_filmstripWidth: filmstripWidth,
_hideScrollbar: Boolean(iAmSipGateway),
_hideToolbar: Boolean(iAmSipGateway),
_hovered: hovered,
_rows: gridDimensions.rows,
_videosClassName: videosClassName,
_visible: visible
};
}
export default translate(connect(_mapStateToProps)(Filmstrip));