123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- /* @flow */
-
- import React, { PureComponent } from 'react';
- import { FixedSizeList, FixedSizeGrid } from 'react-window';
- import type { Dispatch } from 'redux';
-
- import {
- createShortcutEvent,
- createToolbarEvent,
- sendAnalytics
- } from '../../../analytics';
- import { getToolbarButtons } from '../../../base/config';
- import { isMobileBrowser } from '../../../base/environment/utils';
- import { translate } from '../../../base/i18n';
- import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
- import { connect } from '../../../base/redux';
- import { showToolbox } from '../../../toolbox/actions.web';
- import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
- import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
- import { setFilmstripVisible, setVisibleRemoteParticipants } from '../../actions';
- import {
- ASPECT_RATIO_BREAKPOINT,
- TILE_HORIZONTAL_MARGIN,
- TILE_VERTICAL_MARGIN,
- TOOLBAR_HEIGHT,
- TOOLBAR_HEIGHT_MOBILE
- } from '../../constants';
- import { shouldRemoteVideosBeVisible } from '../../functions';
-
- import AudioTracksContainer from './AudioTracksContainer';
- import Thumbnail from './Thumbnail';
- import ThumbnailWrapper from './ThumbnailWrapper';
-
- declare var APP: Object;
- declare var interfaceConfig: Object;
-
- /**
- * 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,
-
- /**
- * The width of the filmstrip.
- */
- _filmstripWidth: number,
-
- /**
- * The height of the filmstrip.
- */
- _filmstripHeight: number,
-
- /**
- * Whether the filmstrip button is enabled.
- */
- _isFilmstripButtonEnabled: boolean,
-
- /**
- * The participants in the call.
- */
- _remoteParticipants: Array<Object>,
-
- /**
- * The length of the remote participants array.
- */
- _remoteParticipantsLength: number,
-
- /**
- * The number of rows in tile view.
- */
- _rows: number,
-
- /**
- * The height of the thumbnail.
- */
- _thumbnailHeight: number,
-
- /**
- * The width of the thumbnail.
- */
- _thumbnailWidth: 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,
-
- /**
- * Whether or not the toolbox is displayed.
- */
- _isToolboxVisible: Boolean,
-
- /**
- * The redux {@code dispatch} function.
- */
- dispatch: Dispatch<any>,
-
- /**
- * Invoked to obtain translated strings.
- */
- t: Function
- };
-
- /**
- * Implements a React {@link Component} which represents the filmstrip on
- * Web/React.
- *
- * @extends Component
- */
- class Filmstrip extends PureComponent <Props> {
-
- /**
- * Initializes a new {@code Filmstrip} instance.
- *
- * @param {Object} props - The read-only properties with which the new
- * instance is to be initialized.
- */
- constructor(props: Props) {
- super(props);
-
- // Bind event handlers so they are only bound once for every instance.
- this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
- this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
- this._onTabIn = this._onTabIn.bind(this);
- this._gridItemKey = this._gridItemKey.bind(this);
- this._listItemKey = this._listItemKey.bind(this);
- this._onGridItemsRendered = this._onGridItemsRendered.bind(this);
- this._onListItemsRendered = this._onListItemsRendered.bind(this);
- }
-
- /**
- * Implements React's {@link Component#componentDidMount}.
- *
- * @inheritdoc
- */
- componentDidMount() {
- 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() {
- const filmstripStyle = { };
- const { _currentLayout } = this.props;
- const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
-
- switch (_currentLayout) {
- case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
- // 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;
- }
-
- let toolbar = null;
-
- if (this.props._isFilmstripButtonEnabled) {
- toolbar = this._renderToggleButton();
- }
-
- return (
- <div
- className = { `filmstrip ${this.props._className}` }
- style = { filmstripStyle }>
- { toolbar }
- <div
- className = { this.props._videosClassName }
- id = 'remoteVideos'>
- <div
- className = 'filmstrip__videos'
- id = 'filmstripLocalVideo'>
- <div id = 'filmstripLocalVideoThumbnail'>
- {
- !tileViewActive && <Thumbnail
- key = 'local' />
- }
- </div>
- </div>
- {
- this._renderRemoteParticipants()
- }
- </div>
- <AudioTracksContainer />
- </div>
- );
- }
-
- _onTabIn: () => void;
-
- /**
- * Toggle the toolbar visibility when tabbing into it.
- *
- * @returns {void}
- */
- _onTabIn() {
- if (!this.props._isToolboxVisible && this.props._visible) {
- this.props.dispatch(showToolbox());
- }
- }
-
- _listItemKey: number => string;
-
- /**
- * The key to be used for every ThumbnailWrapper element in stage view.
- *
- * @param {number} index - The index of the ThumbnailWrapper instance.
- * @returns {string} - The key.
- */
- _listItemKey(index) {
- const { _remoteParticipants, _remoteParticipantsLength } = this.props;
-
- if (typeof index !== 'number' || _remoteParticipantsLength <= index) {
- return `empty-${index}`;
- }
-
- return _remoteParticipants[index];
- }
-
- _gridItemKey: Object => string;
-
- /**
- * The key to be used for every ThumbnailWrapper element in tile views.
- *
- * @param {Object} data - An object with the indexes identifying the ThumbnailWrapper instance.
- * @returns {string} - The key.
- */
- _gridItemKey({ columnIndex, rowIndex }) {
- const { _columns, _remoteParticipants, _remoteParticipantsLength } = this.props;
- const index = (rowIndex * _columns) + columnIndex;
-
- if (index > _remoteParticipantsLength) {
- return `empty-${index}`;
- }
-
- if (index === 0) {
- return 'local';
- }
-
- return _remoteParticipants[index - 1];
- }
-
- _onListItemsRendered: Object => void;
-
- /**
- * Handles items rendered changes in stage view.
- *
- * @param {Object} data - Information about the rendered items.
- * @returns {void}
- */
- _onListItemsRendered({ visibleStartIndex, visibleStopIndex }) {
- const { dispatch } = this.props;
-
- dispatch(setVisibleRemoteParticipants(visibleStartIndex, visibleStopIndex + 1));
- }
-
- _onGridItemsRendered: Object => void;
-
- /**
- * Handles items rendered changes in tile view.
- *
- * @param {Object} data - Information about the rendered items.
- * @returns {void}
- */
- _onGridItemsRendered({
- visibleColumnStartIndex,
- visibleColumnStopIndex,
- visibleRowStartIndex,
- visibleRowStopIndex
- }) {
- const { _columns, dispatch } = this.props;
- let startIndex = (visibleRowStartIndex * _columns) + visibleColumnStartIndex;
- const endIndex = (visibleRowStopIndex * _columns) + visibleColumnStopIndex;
-
- // In tile view, the start index needs to be offset by 1 because the first participant is the local
- // participant.
- startIndex = startIndex > 0 ? startIndex - 1 : 0;
- dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
- }
-
- /**
- * Renders the thumbnails for remote participants.
- *
- * @returns {ReactElement}
- */
- _renderRemoteParticipants() {
- const {
- _columns,
- _currentLayout,
- _filmstripHeight,
- _filmstripWidth,
- _remoteParticipantsLength,
- _rows,
- _thumbnailHeight,
- _thumbnailWidth
- } = this.props;
-
- if (!_thumbnailWidth || isNaN(_thumbnailWidth) || !_thumbnailHeight
- || isNaN(_thumbnailHeight) || !_filmstripHeight || isNaN(_filmstripHeight) || !_filmstripWidth
- || isNaN(_filmstripWidth)) {
- return null;
- }
-
- if (_currentLayout === LAYOUTS.TILE_VIEW) {
- return (
- <FixedSizeGrid
- className = 'filmstrip__videos remote-videos'
- columnCount = { _columns }
- columnWidth = { _thumbnailWidth + TILE_HORIZONTAL_MARGIN }
- height = { _filmstripHeight }
- initialScrollLeft = { 0 }
- initialScrollTop = { 0 }
- itemKey = { this._gridItemKey }
- onItemsRendered = { this._onGridItemsRendered }
- overscanRowCount = { 1 }
- rowCount = { _rows }
- rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN }
- width = { _filmstripWidth }>
- {
- ThumbnailWrapper
- }
- </FixedSizeGrid>
- );
- }
-
-
- const props = {
- itemCount: _remoteParticipantsLength,
- className: 'filmstrip__videos remote-videos',
- height: _filmstripHeight,
- itemKey: this._listItemKey,
- itemSize: 0,
- onItemsRendered: this._onListItemsRendered,
- overscanCount: 1,
- width: _filmstripWidth,
- style: {
- willChange: 'auto'
- }
- };
-
- if (_currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
- const itemSize = _thumbnailWidth + TILE_HORIZONTAL_MARGIN;
- const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripWidth;
-
- props.itemSize = itemSize;
-
- // $FlowFixMe
- props.layout = 'horizontal';
- if (isNotOverflowing) {
- props.className += ' is-not-overflowing';
- }
-
- } else if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
- const itemSize = _thumbnailHeight + TILE_VERTICAL_MARGIN;
- const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripHeight;
-
- if (isNotOverflowing) {
- props.className += ' is-not-overflowing';
- }
-
- props.itemSize = itemSize;
- }
-
- return (
- <FixedSizeList { ...props }>
- {
- ThumbnailWrapper
- }
- </FixedSizeList>
- );
- }
-
- /**
- * Dispatches an action to change the visibility of the filmstrip.
- *
- * @private
- * @returns {void}
- */
- _doToggleFilmstrip() {
- this.props.dispatch(setFilmstripVisible(!this.props._visible));
- }
-
- _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() {
- const icon = this.props._visible ? IconMenuDown : IconMenuUp;
- const { t } = this.props;
-
- return (
- <div
- className = 'filmstrip__toolbar'>
- <button
- aria-expanded = { this.props._visible }
- aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
- id = 'toggleFilmstripButton'
- onClick = { this._onToolbarToggleFilmstrip }
- onFocus = { this._onTabIn }
- tabIndex = { 0 }>
- <Icon
- aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
- 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 toolbarButtons = getToolbarButtons(state);
- const { visible, remoteParticipants } = state['features/filmstrip'];
- const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
- const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
- const { isOpen: shiftRight } = state['features/chat'];
- const {
- gridDimensions = {},
- filmstripHeight,
- filmstripWidth,
- thumbnailSize: tileViewThumbnailSize
- } = state['features/filmstrip'].tileViewDimensions;
- const _currentLayout = getCurrentLayout(state);
-
- const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
- const availableSpace = clientHeight - filmstripHeight;
- let filmstripPadding = 0;
-
- if (availableSpace > 0) {
- const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
-
- if (paddingValue > 0) {
- filmstripPadding = paddingValue;
- }
- } else {
- filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
- }
-
- const collapseTileView = reduceHeight
- && isMobileBrowser()
- && clientWidth <= ASPECT_RATIO_BREAKPOINT;
-
- const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
- reduceHeight ? 'reduce-height' : ''
- } ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''}`.trim();
- const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
- let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
-
- switch (_currentLayout) {
- case LAYOUTS.TILE_VIEW:
- _thumbnailSize = tileViewThumbnailSize;
- remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
- remoteFilmstripWidth = filmstripWidth;
- break;
- case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
- const { remote, remoteVideosContainer } = state['features/filmstrip'].verticalViewDimensions;
-
- _thumbnailSize = remote;
- remoteFilmstripHeight = remoteVideosContainer?.height - (reduceHeight ? TOOLBAR_HEIGHT : 0);
- remoteFilmstripWidth = remoteVideosContainer?.width;
- break;
- }
- case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
- const { remote, remoteVideosContainer } = state['features/filmstrip'].horizontalViewDimensions;
-
- _thumbnailSize = remote;
- remoteFilmstripHeight = remoteVideosContainer?.height;
- remoteFilmstripWidth = remoteVideosContainer?.width;
- break;
- }
- }
-
- return {
- _className: className,
- _columns: gridDimensions.columns,
- _currentLayout,
- _filmstripHeight: remoteFilmstripHeight,
- _filmstripWidth: remoteFilmstripWidth,
- _isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
- _remoteParticipantsLength: remoteParticipants.length,
- _remoteParticipants: remoteParticipants,
- _rows: gridDimensions.rows,
- _thumbnailWidth: _thumbnailSize?.width,
- _thumbnailHeight: _thumbnailSize?.height,
- _videosClassName: videosClassName,
- _visible: visible,
- _isToolboxVisible: isToolboxVisible(state)
- };
- }
-
- export default translate(connect(_mapStateToProps)(Filmstrip));
|