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

641 lines
19 KiB
JavaScript
Raw Normal View History

/* @flow */
2021-03-26 20:23:05 +00:00
import React, { PureComponent } from 'react';
import { FixedSizeList, FixedSizeGrid } from 'react-window';
2019-03-19 15:42:25 +00:00
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';
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 { 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';
2021-03-26 20:23:05 +00:00
import AudioTracksContainer from './AudioTracksContainer';
import Thumbnail from './Thumbnail';
2021-03-26 20:23:05 +00:00
import ThumbnailWrapper from './ThumbnailWrapper';
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,
/**
* The width of the filmstrip.
*/
_filmstripWidth: number,
/**
2021-03-26 20:23:05 +00:00
* The height of the filmstrip.
*/
2021-03-26 20:23:05 +00:00
_filmstripHeight: number,
/**
* Whether this is a recorder or not.
*/
_iAmRecorder: boolean,
/**
* Whether the filmstrip button is enabled.
*/
_isFilmstripButtonEnabled: boolean,
/**
* The participants in the call.
*/
2021-03-26 20:23:05 +00:00
_remoteParticipants: Array<Object>,
/**
* The length of the remote participants array.
*/
_remoteParticipantsLength: number,
/**
* The number of rows in tile view.
*/
_rows: number,
2021-03-26 20:23:05 +00:00
/**
* The height of the thumbnail.
*/
_thumbnailHeight: number,
/**
* The width of the thumbnail.
*/
_thumbnailWidth: number,
/**
* Flag that indicates whether the thumbnails will be reordered.
*/
_thumbnailsReordered: Boolean,
/**
* 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,
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
*/
2021-03-26 20:23:05 +00:00
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.
*/
2018-06-25 15:33:08 +00:00
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);
2021-03-26 20:23:05 +00:00
this._gridItemKey = this._gridItemKey.bind(this);
this._listItemKey = this._listItemKey.bind(this);
this._onGridItemsRendered = this._onGridItemsRendered.bind(this);
this._onListItemsRendered = this._onListItemsRendered.bind(this);
this._onTouchStart = this._onTouchStart.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() {
2020-02-07 15:10:52 +00:00
const filmstripStyle = { };
2021-03-26 20:23:05 +00:00
const { _currentLayout } = this.props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
switch (_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;
}
let toolbar = null;
2021-03-26 20:23:05 +00:00
if (this.props._isFilmstripButtonEnabled) {
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'>
<div id = 'filmstripLocalVideoThumbnail'>
{
!tileViewActive && <Thumbnail
2021-03-26 20:23:05 +00:00
key = 'local' />
}
</div>
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>
2021-03-26 20:23:05 +00:00
{
this._renderRemoteParticipants()
}
</div>
2021-03-26 20:23:05 +00:00
<AudioTracksContainer />
</div>
);
}
/**
* Calculates the start and stop indices based on whether the thumbnails need to be reordered in the filmstrip.
*
* @param {number} startIndex - The start index.
* @param {number} stopIndex - The stop index.
* @returns {Object}
*/
_calculateIndices(startIndex, stopIndex) {
const { _currentLayout, _iAmRecorder, _thumbnailsReordered } = this.props;
let start = startIndex;
let stop = stopIndex;
if (_thumbnailsReordered) {
// In tile view, the indices needs to be offset by 1 because the first thumbnail is that of the local
// endpoint. The remote participants start from index 1.
if (!_iAmRecorder && _currentLayout === LAYOUTS.TILE_VIEW) {
start = Math.max(startIndex - 1, 0);
stop = stopIndex - 1;
}
}
return {
startIndex: start,
stopIndex: stop
};
}
_onTabIn: () => void;
/**
* Toggle the toolbar visibility when tabbing into it.
*
* @returns {void}
*/
_onTabIn() {
if (!this.props._isToolboxVisible && this.props._visible) {
this.props.dispatch(showToolbox());
}
}
2021-03-26 20:23:05 +00:00
_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}`;
}
2021-06-23 18:53:56 +00:00
return _remoteParticipants[index];
2021-03-26 20:23:05 +00:00
}
_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,
_iAmRecorder,
_remoteParticipants,
_remoteParticipantsLength,
_thumbnailsReordered
} = this.props;
2021-03-26 20:23:05 +00:00
const index = (rowIndex * _columns) + columnIndex;
// When the thumbnails are reordered, local participant is inserted at index 0.
const localIndex = _thumbnailsReordered ? 0 : _remoteParticipantsLength;
const remoteIndex = _thumbnailsReordered && !_iAmRecorder ? index - 1 : index;
if (index > _remoteParticipantsLength - (_iAmRecorder ? 1 : 0)) {
2021-03-26 20:23:05 +00:00
return `empty-${index}`;
}
if (!_iAmRecorder && index === localIndex) {
2021-03-26 20:23:05 +00:00
return 'local';
}
return _remoteParticipants[remoteIndex];
2021-03-26 20:23:05 +00:00
}
_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;
const { startIndex, stopIndex } = this._calculateIndices(visibleStartIndex, visibleStopIndex);
dispatch(setVisibleRemoteParticipants(startIndex, stopIndex));
}
_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;
const start = (visibleRowStartIndex * _columns) + visibleColumnStartIndex;
const stop = (visibleRowStopIndex * _columns) + visibleColumnStopIndex;
const { startIndex, stopIndex } = this._calculateIndices(start, stop);
dispatch(setVisibleRemoteParticipants(startIndex, stopIndex));
}
2021-03-26 20:23:05 +00:00
/**
* 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 }
2021-03-26 20:23:05 +00:00
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,
2021-03-26 20:23:05 +00:00
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();
}
_onTouchStart: (SyntheticEvent<HTMLButtonElement>) => void;
/**
* Handler for onTouchStart.
*
* @private
* @param {Object} e - The synthetic event.
* @returns {void}
*/
_onTouchStart(e: SyntheticEvent<HTMLButtonElement>) {
// Don't propagate the touchStart event so the toolbar doesn't get toggled.
e.stopPropagation();
}
/**
* 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-expanded = { this.props._visible }
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
id = 'toggleFilmstripButton'
onClick = { this._onToolbarToggleFilmstrip }
onFocus = { this._onTabIn }
onTouchStart = { this._onTouchStart }
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 { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
2021-03-26 20:23:05 +00:00
const { visible, remoteParticipants } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
2018-06-25 15:33:08 +00:00
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
2021-03-26 20:23:05 +00:00
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'}`;
2021-03-26 20:23:05 +00:00
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
_thumbnailSize = tileViewThumbnailSize;
remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
2021-03-26 20:23:05 +00:00
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 {
2018-06-25 15:33:08 +00:00
_className: className,
_columns: gridDimensions.columns,
2021-03-26 20:23:05 +00:00
_currentLayout,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_iAmRecorder: Boolean(iAmRecorder),
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
2021-03-26 20:23:05 +00:00
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,
_rows: gridDimensions.rows,
2021-03-26 20:23:05 +00:00
_thumbnailWidth: _thumbnailSize?.width,
_thumbnailHeight: _thumbnailSize?.height,
_thumbnailsReordered: enableThumbnailReordering,
_videosClassName: videosClassName,
_visible: visible,
_isToolboxVisible: isToolboxVisible(state)
};
}
export default translate(connect(_mapStateToProps)(Filmstrip));