Compare commits

...

1 Commits

Author SHA1 Message Date
Hristo Terezov
9e4f9f3c2e feat(Filmstrip): Pagination. 2021-05-06 15:28:49 -05:00
16 changed files with 741 additions and 415 deletions

View File

@@ -33,18 +33,18 @@
}
&__videos {
@extend %align-right;
position:relative;
padding: 0;
/* The filmstrip should not be covered by the left toolbar. */
bottom: 0;
width:auto;
overflow: visible !important;
&#remoteVideos {
border: $thumbnailsBorder solid transparent;
transition: bottom 2s;
flex-grow: 1;
display: flex;
flex-direction: row-reverse;
@include minHWAutoFix()
}
@@ -60,41 +60,25 @@
&.hidden {
bottom: calc(-196px - #{$newToolbarSizeWithPadding});
}
.remote-videos-container {
display: flex;
}
}
.remote-videos-container {
transition: opacity 1s;
.remote-videos {
& > div {
transition: opacity 1s;
position: absolute;
}
&.is-not-overflowing > div {
right: 2px;
}
}
&.hide-videos {
.remote-videos-container {
opacity: 0;
pointer-events: none;
}
}
#filmstripRemoteVideos {
@include minHWAutoFix();
display: flex;
flex: 1;
width: auto;
justify-content: flex-end;
flex-direction: row;
#filmstripRemoteVideosContainer {
flex-direction: row-reverse;
/**
* Add padding as a hack for Firefox not to show scrollbars when
* unnecessary.
*/
padding: 1px 0;
overflow-y: hidden;
overflow-x: scroll;
.remote-videos {
& > div {
opacity: 0;
pointer-events: none;
}
}
}
@@ -103,25 +87,3 @@
}
}
/**
* Workarounds for Edge and Firefox not handling scrolling properly with
* flex-direction: row-reverse.
*/
@mixin undoRowReverseVideos() {
.horizontal-filmstrip {
#remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
flex-direction: row;
}
}
}
/** Firefox detection hack **/
@-moz-document url-prefix() {
@include undoRowReverseVideos();
}
/** Edge detection hack **/
@supports (-ms-ime-align:auto) {
@include undoRowReverseVideos();
}

View File

@@ -10,13 +10,11 @@
box-shadow: 0 0 5px 3px $videoThumbnailSelected
}
#filmstripRemoteVideos {
.remote-videos {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
width: 100vw;
}
.filmstrip__videos .videocontainer {
@@ -34,6 +32,9 @@
*/
height: 100% !important;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.filmstrip {
@@ -51,7 +52,7 @@
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
#filmstripRemoteVideos {
.remote-videos{
width: calc(100vw - #{$sidebarWidth});
}
}
@@ -66,63 +67,49 @@
display: block;
}
#filmstripRemoteVideos {
.remote-videos {
box-sizing: border-box;
/**
* Allow vertical scrolling of the thumbnails.
*/
overflow-x: hidden;
overflow-y: auto;
}
/**
* The size of the thumbnails should be set with javascript, based on
* desired column count and window width. The rows are created using flex
* and allowing the thumbnails to wrap.
*/
#filmstripRemoteVideosContainer {
align-content: center;
align-items: center;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
flex-shrink: 0;
margin-top: auto;
margin-bottom: auto;
justify-content: center;
.videocontainer {
border: 0;
* The size of the thumbnails should be set with javascript, based on
* desired column count and window width. The rows are created using flex
* and allowing the thumbnails to wrap.
*/
& > div {
align-content: center;
align-items: center;
box-sizing: border-box;
display: block;
margin: 2px;
}
display: flex;
margin-top: auto;
margin-bottom: auto;
justify-content: center;
position: absolute;
video {
object-fit: contain;
}
.videocontainer {
border: 0;
box-sizing: border-box;
display: block;
margin: 2px;
}
/**
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
*/
@media only screen and (max-width: 500px) {
video {
object-fit: cover;
object-fit: contain;
}
/**
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
*/
@media only screen and (max-width: 500px) {
video {
object-fit: cover;
}
}
}
}
.has-overflow#filmstripRemoteVideosContainer {
align-content: baseline;
}
.has-overflow .videocontainer {
align-self: baseline;
}
}
.shift-right #filmstripRemoteVideosContainer {
.shift-right .remote-videos > div {
/**
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants,
* from which we subtract the chat size.

View File

@@ -1,8 +1,10 @@
.vertical-filmstrip .filmstrip {
&.hide-videos {
.remote-videos-container {
opacity: 0;
pointer-events: none;
.remote-videos {
& > div {
opacity: 0;
pointer-events: none;
}
}
}
@@ -39,10 +41,6 @@
right: 0;
z-index: $filmstripVideosZ;
&.reduce-height {
height: calc(100% - #{$newToolbarSizeWithPadding});
}
/**
* Hide videos by making them slight to the right.
*/
@@ -98,33 +96,10 @@
* filmstrip from overlapping the left edge of the screen.
*/
#filmstripLocalVideo,
#filmstripRemoteVideos {
.remote-videos {
padding: 0;
}
#filmstripRemoteVideos {
@include minHWAutoFix();
display: flex;
flex: 1;
flex-direction: column-reverse;
height: auto;
overflow-x: hidden;
overflow-y: scroll;
#filmstripRemoteVideosContainer {
@include minHWAutoFix();
flex-direction: column-reverse;
overflow: visible;
width: calc(100% - 8px); // 8px for margin + border of the thumbnails
.videocontainer {
height: 0px;
width: 100%;
}
}
}
#remoteVideos {
@include minHWAutoFix();
@@ -132,56 +107,21 @@
flex-grow: 1;
}
.remote-videos-container {
&.reduce-height {
height: calc(100% - calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight}));
}
.remote-videos {
display: flex;
transition: opacity 1s;
}
transition: height .3s ease-in;
.hide-scrollbar#filmstripRemoteVideos {
margin-right: 7px; // Scrollbar size
&::-webkit-scrollbar {
display: none;
& > div {
position: absolute;
transition: opacity 1s;
}
&.is-not-overflowing > div {
bottom: 0px;
}
}
}
/**
* Workarounds for Edge and Firefox not handling scrolling properly with
* flex-direction: column-reverse. The remove videos in filmstrip should
* start scrolling from the bottom of the filmstrip, but in those browsers the
* scrolling won't happen. Per W3C spec, scrolling should happen from the
* bottom. As such, use css hacks to get around the css issue, with the intent
* being to remove the hacks as the spec is supported.
*/
@mixin undoColumnReverseVideos() {
.vertical-filmstrip {
#remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
flex-direction: column;
}
}
}
/**
* FF does not include the scroll width when calculating the size of the content. That's why we need to include
* ourselves the width of the scroll so that the remote videos are aligned with the local one.
*/
@mixin filmstripSizeWithoutScroll {
.vertical-filmstrip {
#remoteVideos #filmstripRemoteVideos {
#filmstripRemoteVideosContainer {
width: calc(100% - 15px) // 8 px - margins + border of the thumbnails; 7px - for the scroll
}
}
}
}
/** Firefox detection hack **/
@-moz-document url-prefix() {
@include undoColumnReverseVideos();
@include filmstripSizeWithoutScroll();
}
/** Edge detection hack **/
@supports (-ms-ime-align:auto) {
@include undoColumnReverseVideos();
}

14
package-lock.json generated
View File

@@ -10826,6 +10826,11 @@
}
}
},
"memoize-one": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
},
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@@ -14625,6 +14630,15 @@
}
}
},
"react-window": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
"integrity": "sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==",
"requires": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
}
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",

View File

@@ -89,6 +89,7 @@
"react-redux": "7.1.0",
"react-textarea-autosize": "7.1.0",
"react-transition-group": "2.4.0",
"react-window": "^1.8.6",
"redux": "4.0.4",
"redux-thunk": "2.2.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",

View File

@@ -27,7 +27,7 @@ export const SET_FILMSTRIP_VISIBLE = 'SET_FILMSTRIP_VISIBLE';
* gridDimensions: {
* columns: number,
* height: number,
* visibleRows: number,
* minVisibleRows: number,
* width: number
* },
* thumbnailSize: {
@@ -49,3 +49,25 @@ export const SET_TILE_VIEW_DIMENSIONS = 'SET_TILE_VIEW_DIMENSIONS';
* }
*/
export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS';
/**
* The type of (redux) action which sets the dimensions of the thumbnails in vertical view.
*
* {
* type: SET_VERTICAL_VIEW_DIMENSIONS,
* dimensions: Object
* }
*/
export const SET_VERTICAL_VIEW_DIMENSIONS = 'SET_VERTICAL_VIEW_DIMENSIONS';
/**
* The type of (redux) action which sets the thumbnails rendered.
*
* {
* type: SET_FILMSTRIP_ITEMS_RENDERED,
* startIndex: number,
* endIndex: number,
* }
*/
export const SET_FILMSTRIP_ITEMS_RENDERED = 'SET_FILMSTRIP_ITEMS_RENDERED';

View File

@@ -1,64 +1,112 @@
// @flow
import type { Dispatch } from 'redux';
import { pinParticipant } from '../base/participants';
import { toState } from '../base/redux';
import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
import { calculateThumbnailSizeForHorizontalView, calculateThumbnailSizeForTileView } from './functions';
/**
* The size of the side margins for the entire tile view area.
*/
const TILE_VIEW_SIDE_MARGINS = 20;
import {
SET_FILMSTRIP_ITEMS_RENDERED,
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS,
SET_VERTICAL_VIEW_DIMENSIONS
} from './actionTypes';
import { SCROLL_SIZE, TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN } from './constants';
import {
calculateThumbnailSizeForHorizontalView,
calculateThumbnailSizeForTileView,
calculateThumbnailSizeForVerticalView
} from './functions';
/**
* Sets the dimensions of the tile view grid.
*
* @param {Object} dimensions - Whether the filmstrip is visible.
* @param {Object} windowSize - The size of the window.
* @param {Object | Function} stateful - An object or function that can be
* resolved to Redux state using the {@code toState} function.
* @returns {{
* type: SET_TILE_VIEW_DIMENSIONS,
* dimensions: Object
* }}
* @returns {Function}
*/
export function setTileViewDimensions(dimensions: Object, windowSize: Object, stateful: Object | Function) {
const state = toState(stateful);
const { clientWidth, clientHeight } = windowSize;
const { disableResponsiveTiles } = state['features/base/config'];
export function setTileViewDimensions(dimensions: Object) {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const { disableResponsiveTiles } = state['features/base/config'];
const {
height,
width
} = calculateThumbnailSizeForTileView({
...dimensions,
clientWidth,
clientHeight,
disableResponsiveTiles
});
const { columns, rows } = dimensions;
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
const filmstripWidth = (columns * (TILE_HORIZONTAL_MARGIN + width)) + (hasScroll ? SCROLL_SIZE : 0);
const filmstripHeight = Math.min(clientHeight, thumbnailsTotalHeight);
const thumbnailSize = calculateThumbnailSizeForTileView({
...dimensions,
clientWidth,
clientHeight,
disableResponsiveTiles
});
const filmstripWidth = dimensions.columns * (TILE_VIEW_SIDE_MARGINS + thumbnailSize.width);
dispatch({
type: SET_TILE_VIEW_DIMENSIONS,
dimensions: {
gridDimensions: dimensions,
thumbnailSize: {
height,
width
},
filmstripHeight,
filmstripWidth
}
});
};
}
return {
type: SET_TILE_VIEW_DIMENSIONS,
dimensions: {
gridDimensions: dimensions,
thumbnailSize,
filmstripWidth
}
/**
* Sets the dimensions of the thumbnails in vertical view.
*
* @returns {Function}
*/
export function setVerticalViewDimensions() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
const thumbnails = calculateThumbnailSizeForVerticalView(clientWidth);
dispatch({
type: SET_VERTICAL_VIEW_DIMENSIONS,
dimensions: {
...thumbnails,
remoteVideosContainer: {
// 2px for border + 2px margin for left and right + 7px for scroll
width: thumbnails?.local?.width + 15,
height: clientHeight - thumbnails?.local?.height - 60
}
}
});
};
}
/**
* Sets the dimensions of the thumbnails in horizontal view.
*
* @param {number} clientHeight - The height of the window.
* @returns {{
* type: SET_HORIZONTAL_VIEW_DIMENSIONS,
* dimensions: Object
* }}
* @returns {Function}
*/
export function setHorizontalViewDimensions(clientHeight: number = 0) {
return {
type: SET_HORIZONTAL_VIEW_DIMENSIONS,
dimensions: calculateThumbnailSizeForHorizontalView(clientHeight)
export function setHorizontalViewDimensions() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
const thumbnails = calculateThumbnailSizeForHorizontalView(clientHeight);
dispatch({
type: SET_HORIZONTAL_VIEW_DIMENSIONS,
dimensions: {
...thumbnails,
remoteVideosContainer: {
// 39px for margins, borders, paddings and the hide filmstrip icon
width: clientWidth - thumbnails?.local?.width - 39,
height: thumbnails?.local?.height + 14 // margin + borders
}
}
});
};
}
@@ -78,4 +126,17 @@ export function clickOnVideo(n: number) {
};
}
/**
*
* @param {number} startIndex
* @param {number} endIndex
*/
export function setFilmstripItemsRendered(startIndex, endIndex) {
return {
type: SET_FILMSTRIP_ITEMS_RENDERED,
startIndex,
endIndex
};
}
export * from './actions.native';

View File

@@ -1,6 +1,7 @@
/* @flow */
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import { FixedSizeList, FixedSizeGrid } from 'react-window';
import type { Dispatch } from 'redux';
import {
@@ -11,18 +12,22 @@ import {
import { getToolbarButtons } from '../../../base/config';
import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { isButtonEnabled } from '../../../toolbox/functions.web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { setFilmstripVisible } from '../../actions';
import { setFilmstripItemsRendered } from '../../actions.web';
import { TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN } from '../../constants';
import { shouldRemoteVideosBeVisible } from '../../functions';
import Thumbnail from './Thumbnail';
import ThumbnailWrapper from './ThumbnailWrapper';
declare var APP: Object;
declare var interfaceConfig: Object;
const TOOLBAR_SIZE = 72;
/**
* The type of the React {@code Component} props of {@link Filmstrip}.
*/
@@ -48,15 +53,7 @@ type Props = {
*/
_filmstripWidth: number,
/**
* Whether the filmstrip scrollbar should be hidden or not.
*/
_hideScrollbar: boolean,
/**
* Whether the filmstrip toolbar should be hidden or not.
*/
_hideToolbar: boolean,
_filmstripHeight: number,
/**
* Whether the filmstrip button is enabled.
@@ -66,12 +63,9 @@ type Props = {
/**
* The participants in the call.
*/
_participants: Array<Object>,
_remoteParticipants: Array<Object>,
/**
* The number of rows in tile view.
*/
_rows: number,
_thumbnailSize: Object,
/**
* Additional CSS class names to add to the container of all the thumbnails.
@@ -100,7 +94,7 @@ type Props = {
*
* @extends Component
*/
class Filmstrip extends Component <Props> {
class Filmstrip extends PureComponent <Props> {
/**
* Initializes a new {@code Filmstrip} instance.
@@ -114,6 +108,10 @@ class Filmstrip extends Component <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._onGridItemsRendered = this._onGridItemsRendered.bind(this);
this._onListItemsRendered = this._onListItemsRendered.bind(this);
this._gridItemKey = this._gridItemKey.bind(this);
this._listItemKey = this._listItemKey.bind(this);
}
/**
@@ -147,11 +145,7 @@ class Filmstrip extends Component <Props> {
*/
render() {
const filmstripStyle = { };
const filmstripRemoteVideosContainerStyle = {};
let remoteVideoContainerClassName = 'remote-videos-container';
const { _currentLayout, _participants } = this.props;
const remoteParticipants = _participants.filter(p => !p.local);
const localParticipant = getLocalParticipant(_participants);
const { _currentLayout } = this.props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
switch (_currentLayout) {
@@ -160,28 +154,11 @@ class Filmstrip extends Component <Props> {
// 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 && this.props._isFilmstripButtonEnabled) {
if (this.props._isFilmstripButtonEnabled) {
toolbar = this._renderToggleButton();
}
@@ -199,45 +176,192 @@ class Filmstrip extends Component <Props> {
<div id = 'filmstripLocalVideoThumbnail'>
{
!tileViewActive && <Thumbnail
key = 'local'
participantID = { localParticipant.id } />
key = 'local' />
}
</div>
</div>
<div
className = { remoteVideosWrapperClassName }
id = 'filmstripRemoteVideos'>
{/*
* 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'
style = { filmstripRemoteVideosContainerStyle }>
{
remoteParticipants.map(
p => (
<Thumbnail
key = { `remote_${p.id}` }
participantID = { p.id } />
))
}
<div id = 'localVideoTileViewContainer'>
{
tileViewActive && <Thumbnail
key = 'local'
participantID = { localParticipant.id } />
}
</div>
</div>
</div>
{
this._renderRemoteParticipants()
}
</div>
</div>
);
}
/**
*
* @param {*} index
*/
_listItemKey(index) {
const { _remoteParticipants, _remoteParticipantsLength } = this.props;
if (typeof index !== 'number' || _remoteParticipantsLength <= index) {
return `empty-${index}`;
}
return _remoteParticipants[_remoteParticipantsLength - index - 1];
}
/**
*
* @param {*} param0
*/
_gridItemKey({ columnIndex, data, rowIndex }) {
const { _columns, _remoteParticipants, _remoteParticipantsLength } = this.props;
const index = (rowIndex * _columns) + columnIndex;
if (index > _remoteParticipantsLength) {
return `empty-${index}`;
}
if (index === _remoteParticipantsLength) {
return 'local';
}
return _remoteParticipants[index];
}
/**
*
* @param {*} param0
* @returns {void}
*/
_onListItemsRendered({ overscanStartIndex, overscanStopIndex, visibleStartIndex, visibleStopIndex }) {
const { dispatch, _remoteParticipantsLength } = this.props;
// startIndex and endIndex are swapped because we show the thumbnails in reverse order.
const endIndex = _remoteParticipantsLength - overscanStartIndex;
const startIndex = Math.max(_remoteParticipantsLength - overscanStopIndex, 0);
dispatch(setFilmstripItemsRendered(startIndex, endIndex));
console.error(`_onListItemsRendered ${overscanStartIndex}, ${overscanStopIndex}, ${
visibleStartIndex}, ${visibleStopIndex}`);
}
/**
*
* @param {*} param0
* @returns {void}
*/
_onGridItemsRendered({
overscanColumnStartIndex,
overscanColumnStopIndex,
overscanRowStartIndex,
overscanRowStopIndex,
visibleColumnStartIndex,
visibleColumnStopIndex,
visibleRowStartIndex,
visibleRowStopIndex
}) {
const { columns, dispatch, _remoteParticipantsLength } = this.props;
const startIndex = (overscanRowStartIndex * columns) + overscanColumnStartIndex + 1;
const endIndex = Math.min(
(overscanRowStopIndex * columns) + overscanColumnStopIndex + 1,
_remoteParticipantsLength - 1);
dispatch(setFilmstripItemsRendered(startIndex, endIndex));
console.error(`_onListItemsRendered ${overscanColumnStartIndex}, ${overscanColumnStopIndex}, ${
overscanRowStartIndex}, ${overscanRowStopIndex}, ${visibleColumnStartIndex} , ${visibleColumnStopIndex}, ${
visibleRowStartIndex}, ${visibleRowStopIndex}`);
// 0, 4, 2, 8, 0 , 4, 3, 8
// 0, 4, 2, 7, 0 , 4, 3, 7
// 0, 4, 4, 11, 0 , 4, 5, 10
// 0, 4, 4, 10, 0 , 4, 5, 10
}
/**
* Renders the thumbnails for remote participants.
*
* @returns {ReactElement}
*/
_renderRemoteParticipants() {
const {
_columns,
_currentLayout,
_filmstripHeight,
_filmstripWidth,
_remoteParticipantsLength,
_rows,
_thumbnailSize = {},
_toolbarVisible
} = this.props;
const { height: thumbnailHeight, width: thumbnailWidth } = _thumbnailSize;
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 }
rowCount = { _rows }
rowHeight = { thumbnailHeight + TILE_VERTICAL_MARGIN }
width = { _filmstripWidth }>
{
ThumbnailWrapper
}
</FixedSizeGrid>
);
}
const props = {
itemCount: _remoteParticipantsLength,
className: 'filmstrip__videos remote-videos',
height: _filmstripHeight,
onItemsRendered: this._onListItemsRendered,
itemKey: this._listItemKey,
width: _filmstripWidth,
style: {
willChange: 'auto'
}
};
if (_currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
const itemSize = thumbnailHeight + TILE_VERTICAL_MARGIN;
const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripWidth;
props.itemSize = itemSize;
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;
props.height = _filmstripHeight - (_toolbarVisible ? TOOLBAR_SIZE : 0);
}
return (
<FixedSizeList { ...props }>
{
ThumbnailWrapper
}
</FixedSizeList>
);
}
/**
* Dispatches an action to change the visibility of the filmstrip.
*
@@ -318,29 +442,60 @@ class Filmstrip extends Component <Props> {
* @returns {Props}
*/
function _mapStateToProps(state) {
const { iAmSipGateway } = state['features/base/config'];
const toolbarButtons = getToolbarButtons(state);
const { visible } = state['features/filmstrip'];
const reduceHeight
= state['features/toolbox'].visible && toolbarButtons.length;
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 className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
reduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''}`.trim();
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
const {
gridDimensions = {},
filmstripHeight,
filmstripWidth,
thumbnailSize: tileViewThumbnailSize
} = state['features/filmstrip'].tileViewDimensions;
const _currentLayout = getCurrentLayout(state);
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
_thumbnailSize = tileViewThumbnailSize;
remoteFilmstripHeight = filmstripHeight;
remoteFilmstripWidth = filmstripWidth;
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
const { remote, remoteVideosContainer } = state['features/filmstrip'].verticalViewDimensions;
_thumbnailSize = remote;
remoteFilmstripHeight = remoteVideosContainer?.height;
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: getCurrentLayout(state),
_filmstripWidth: filmstripWidth,
_hideScrollbar: Boolean(iAmSipGateway),
_hideToolbar: Boolean(iAmSipGateway),
_currentLayout,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_participants: state['features/base/participants'],
_remoteParticipantsLength: state['features/base/participants'].length - 1,
_remoteParticipants: remoteParticipants,
_rows: gridDimensions.rows,
_thumbnailSize,
_toolbarVisible: reduceHeight,
_videosClassName: videosClassName,
_visible: visible
};

View File

@@ -181,7 +181,7 @@ export type Props = {|
/**
* The number of participants in the call.
*/
_participantCount: number,
_participantCountMoreThan2: boolean,
/**
* Indicates whether the "start silent" mode is enabled.
@@ -206,7 +206,11 @@ export type Props = {|
/**
* The ID of the participant related to the thumbnail.
*/
participantID: ?string
participantID: ?string,
style?: ?Object,
verticalOffset: number
|};
/**
@@ -457,7 +461,7 @@ class Thumbnail extends Component<Props, State> {
* @returns {Object} - The styles for the thumbnail.
*/
_getStyles(): Object {
const { _height, _heightToWidthPercent, _currentLayout, _isHidden, _width } = this.props;
const { _height, _isHidden, _width, style = {}, verticalOffset } = this.props;
let styles: {
thumbnail: Object,
avatar: Object
@@ -466,39 +470,28 @@ class Thumbnail extends Component<Props, State> {
avatar: {}
};
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const avatarSize = _height / 2;
const avatarSize = _height / 2;
let { left } = style;
styles = {
thumbnail: {
height: `${_height}px`,
minHeight: `${_height}px`,
minWidth: `${_width}px`,
width: `${_width}px`
},
avatar: {
height: `${avatarSize}px`,
width: `${avatarSize}px`
}
};
break;
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
styles = {
thumbnail: {
paddingTop: `${_heightToWidthPercent}%`
},
avatar: {
height: '50%',
width: `${_heightToWidthPercent / 2}%`
}
};
break;
}
if (typeof left === 'number' && verticalOffset) {
left += verticalOffset;
}
styles = {
thumbnail: {
...style,
left,
height: `${_height}px`,
minHeight: `${_height}px`,
minWidth: `${_width}px`,
width: `${_width}px`
},
avatar: {
height: `${avatarSize}px`,
width: `${avatarSize}px`
}
};
if (_isHidden) {
styles.thumbnail.display = 'none';
}
@@ -587,7 +580,7 @@ class Thumbnail extends Component<Props, State> {
_isDominantSpeakerDisabled,
_indicatorIconSize: iconSize,
_participant,
_participantCount
_participantCountMoreThan2
} = this.props;
const { isHovered } = this.state;
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
@@ -624,7 +617,7 @@ class Thumbnail extends Component<Props, State> {
iconSize = { iconSize }
participantId = { id }
tooltipPosition = { tooltipPosition } />
{ showDominantSpeaker && _participantCount > 2
{ showDominantSpeaker && _participantCountMoreThan2
&& <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
@@ -970,14 +963,21 @@ function _mapStateToProps(state, ownProps): Object {
switch (_currentLayout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const {
horizontalViewDimensions = {
local: {},
remote: {}
},
verticalViewDimensions = {
local: {},
remote: {}
}
} = state['features/filmstrip'];
const { local, remote } = horizontalViewDimensions;
const { local, remote }
= _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
? verticalViewDimensions : horizontalViewDimensions;
const { width, height } = isLocal ? local : remote;
size = {
@@ -987,13 +987,6 @@ function _mapStateToProps(state, ownProps): Object {
break;
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
size = {
_heightToWidthPercent: isLocal
? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
: 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
};
break;
case LAYOUTS.TILE_VIEW: {
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
@@ -1023,7 +1016,7 @@ function _mapStateToProps(state, ownProps): Object {
_indicatorIconSize: NORMAL,
_localFlipX: Boolean(localFlipX),
_participant: participant,
_participantCount: getParticipantCount(state),
_participantCountMoreThan2: getParticipantCount(state) > 2,
_startSilent: Boolean(startSilent),
_videoTrack,
...size

View File

@@ -0,0 +1,125 @@
/* @flow */
import React, { Component } from 'react';
import { shouldComponentUpdate } from 'react-window';
import { connect } from '../../../base/redux';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import Thumbnail from './Thumbnail';
/**
* The type of the React {@code Component} props of {@link Filmstrip}.
*/
type Props = {
columnIndex?: number,
_participantID: ?string,
rowIndex?: number,
index: number,
style: Object,
_verticalOffset: number
};
/**
*
*/
class ThumbnailWrapper extends Component<Props> {
/**
*
* @param {Props} props
*/
constructor(props: Props) {
super(props);
this.shouldComponentUpdate = shouldComponentUpdate.bind(this);
}
/**
*
* @returns
*/
render() {
const { _participantID, style, _verticalOffset = 0 } = this.props;
if (typeof _participantID !== 'string') {
return null;
}
if (_participantID === 'local') {
return (
<Thumbnail
key = 'local'
style = { style }
verticalOffset = { _verticalOffset } />);
}
return (
<Thumbnail
key = { `remote_${_participantID}` }
participantID = { _participantID }
style = { style }
verticalOffset = { _verticalOffset } />);
}
}
/**
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The props passed to the component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const _currentLayout = getCurrentLayout(state);
const { remoteParticipants } = state['features/filmstrip'];
const remoteParticipantsLength = remoteParticipants.length;
if (_currentLayout === LAYOUTS.TILE_VIEW) {
const { columnIndex, rowIndex } = ownProps;
const { gridDimensions = {}, thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
const { columns, rows } = gridDimensions;
const index = (rowIndex * columns) + columnIndex;
let verticalOffset;
if (rowIndex === rows - 1) { // center the last row
const { width: thumbnailWidth } = thumbnailSize;
const participantsInTheLastRow = (remoteParticipantsLength + 1) % columns;
if (participantsInTheLastRow > 0) {
verticalOffset = Math.floor((columns - participantsInTheLastRow) * (thumbnailWidth + 4) / 2);
}
}
if (index > remoteParticipantsLength) {
return {};
}
if (index === remoteParticipantsLength) {
return {
_participantID: 'local',
_verticalOffset: verticalOffset
};
}
return {
_participantID: remoteParticipants[index],
_verticalOffset: verticalOffset
};
}
const { index } = ownProps;
if (typeof index !== 'number' || remoteParticipantsLength <= index) {
return {};
}
return {
_participantID: remoteParticipants[remoteParticipantsLength - index - 1]
};
}
export default connect(_mapStateToProps)(ThumbnailWrapper);

View File

@@ -143,3 +143,14 @@ export const DISPLAY_MODE_TO_STRING = [
'video-with-name',
'avatar-with-name'
];
/**
* Minimum space to keep between the sides of the tiles and the sides of the window.
* @type {number}
*/
export const TILE_VIEW_SIDE_MARGINS = 7;
export const TILE_VERTICAL_MARGIN = 4;
export const TILE_HORIZONTAL_MARGIN = 4;
export const SCROLL_SIZE = 7;

View File

@@ -23,16 +23,16 @@ import {
DISPLAY_BLACKNESS_WITH_NAME,
DISPLAY_VIDEO,
DISPLAY_VIDEO_WITH_NAME,
SCROLL_SIZE,
SQUARE_TILE_ASPECT_RATIO,
TILE_ASPECT_RATIO
TILE_ASPECT_RATIO,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
TILE_VIEW_SIDE_MARGINS
} from './constants';
declare var interfaceConfig: Object;
// Minimum space to keep between the sides of the tiles and the sides
// of the window.
const TILE_VIEW_SIDE_MARGINS = 20;
/**
* Returns true if the filmstrip on mobile is visible, false otherwise.
*
@@ -137,15 +137,43 @@ export function calculateThumbnailSizeForHorizontalView(clientHeight: number = 0
};
}
/**
* Calculates the size for thumbnails when in vertical view layout.
*
* @param {number} clientWidth - The height of the app window.
* @returns {{local: {height, width}, remote: {height, width}}}
*/
export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0) {
// thumbnail: 2px margin + 2px border - left and right
// scroll: width 7px
// .filmstrip: 5px padding left and right
const horizontalMargin = 25;
const availableWidth = Math.min(
Math.max(clientWidth - horizontalMargin, 0),
interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120);
return {
local: {
height: Math.floor(availableWidth / interfaceConfig.LOCAL_THUMBNAIL_RATIO),
width: availableWidth
},
remote: {
height: Math.floor(availableWidth / interfaceConfig.REMOTE_THUMBNAIL_RATIO),
width: availableWidth
}
};
}
/**
* Calculates the size for thumbnails when in tile view layout.
*
* @param {Object} dimensions - The desired dimensions of the tile view grid.
* @returns {{height, width}}
* @returns {{hasScroll, height, width}}
*/
export function calculateThumbnailSizeForTileView({
columns,
visibleRows,
minVisibleRows,
rows,
clientWidth,
clientHeight,
disableResponsiveTiles
@@ -156,12 +184,29 @@ export function calculateThumbnailSizeForTileView({
aspectRatio = SQUARE_TILE_ASPECT_RATIO;
}
const viewWidth = clientWidth - TILE_VIEW_SIDE_MARGINS;
const viewHeight = clientHeight - TILE_VIEW_SIDE_MARGINS;
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN);
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN);
const initialWidth = viewWidth / columns;
const initialHeight = viewHeight / minVisibleRows;
const aspectRatioHeight = initialWidth / aspectRatio;
const height = Math.floor(Math.min(aspectRatioHeight, viewHeight / visibleRows));
const width = Math.floor(aspectRatio * height);
const noScrollHeight = (clientHeight / rows) - TILE_VERTICAL_MARGIN;
const scrollInitialWidth = (viewWidth - SCROLL_SIZE) / columns;
let height = Math.floor(Math.min(aspectRatioHeight, initialHeight));
let width = Math.floor(aspectRatio * height);
if (height > noScrollHeight && width > scrollInitialWidth) { // we will have scroll and we need more space for it.
const scrollAspectRatioHeight = scrollInitialWidth / aspectRatio;
// Recalculating width/height to fit the available space when a scroll is displayed.
// NOTE: Math.min(scrollAspectRatioHeight, initialHeight) would be enough to recalculate but since the new
// height value can theoretically be dramatically smaller and the scroll may not be neccessary anymore we need
// to compare it with noScrollHeight( the optimal height to fit all thumbnails without scroll) and get the
// bigger one. This way we ensure that we always strech the thumbnails as close as we can to the edges of the
// window.
height = Math.floor(Math.max(Math.min(scrollAspectRatioHeight, initialHeight), noScrollHeight));
width = Math.floor(aspectRatio * height);
}
return {
height,

View File

@@ -9,7 +9,7 @@ import {
LAYOUTS
} from '../video-layout';
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
import './subscriber.web';
@@ -27,22 +27,16 @@ MiddlewareRegistry.register(store => next => action => {
switch (layout) {
case LAYOUTS.TILE_VIEW: {
const { gridDimensions } = state['features/filmstrip'].tileViewDimensions;
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
store.dispatch(
setTileViewDimensions(
gridDimensions,
{
clientHeight,
clientWidth
},
store
)
);
store.dispatch(setTileViewDimensions(gridDimensions));
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
store.dispatch(setHorizontalViewDimensions());
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
store.dispatch(setVerticalViewDimensions());
break;
}
break;

View File

@@ -1,12 +1,15 @@
// @flow
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
import { ReducerRegistry } from '../base/redux';
import {
SET_FILMSTRIP_ENABLED,
SET_FILMSTRIP_ITEMS_RENDERED,
SET_FILMSTRIP_VISIBLE,
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS
SET_TILE_VIEW_DIMENSIONS,
SET_VERTICAL_VIEW_DIMENSIONS
} from './actionTypes';
const DEFAULT_STATE = {
@@ -40,7 +43,9 @@ const DEFAULT_STATE = {
* @public
* @type {boolean}
*/
visible: true
visible: true,
remoteParticipants: []
};
ReducerRegistry.register(
@@ -69,6 +74,46 @@ ReducerRegistry.register(
...state,
tileViewDimensions: action.dimensions
};
case SET_VERTICAL_VIEW_DIMENSIONS:
return {
...state,
verticalViewDimensions: action.dimensions
};
case SET_FILMSTRIP_ITEMS_RENDERED:
return {
...state,
startIndex: action.startIndex,
endIndex: action.endIndex
};
case PARTICIPANT_JOINED: {
const { id, local } = action.participant;
if (!local) {
state.remoteParticipants.push(id);
}
return state;
}
case PARTICIPANT_LEFT: {
const { id, local } = action.participant;
if (local) {
return state;
}
const { remoteParticipants } = state;
const index = remoteParticipants.findIndex(participantId => participantId === id);
if (index === -1) {
return state;
}
remoteParticipants[index] = remoteParticipants[remoteParticipants.length - 1];
remoteParticipants.pop();
return state;
}
}
return state;

View File

@@ -6,7 +6,7 @@ import { setFilmstripVisible } from '../filmstrip/actions';
import { setOverflowDrawer } from '../toolbox/actions.web';
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
import {
ASPECT_RATIO_BREAKPOINT,
DISPLAY_DRAWER_THRESHOLD,
@@ -27,18 +27,7 @@ StateListenerRegistry.register(
const oldGridDimensions = state['features/filmstrip'].tileViewDimensions.gridDimensions;
if (!equals(gridDimensions, oldGridDimensions)) {
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
store.dispatch(
setTileViewDimensions(
gridDimensions,
{
clientHeight,
clientWidth
},
store
)
);
store.dispatch(setTileViewDimensions(gridDimensions));
}
}
});
@@ -52,23 +41,14 @@ StateListenerRegistry.register(
const state = store.getState();
switch (layout) {
case LAYOUTS.TILE_VIEW: {
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
store.dispatch(
setTileViewDimensions(
getTileViewGridDimensions(state),
{
clientHeight,
clientWidth
},
store
)
);
case LAYOUTS.TILE_VIEW:
store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
store.dispatch(setHorizontalViewDimensions());
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
store.dispatch(setVerticalViewDimensions());
break;
}
});
@@ -149,17 +129,7 @@ StateListenerRegistry.register(
if (shouldDisplayTileView(state)) {
const gridDimensions = getTileViewGridDimensions(state);
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
store.dispatch(
setTileViewDimensions(
gridDimensions,
{
clientHeight,
clientWidth
},
store
)
);
store.dispatch(setTileViewDimensions(gridDimensions));
}
});

View File

@@ -88,11 +88,12 @@ export function getTileViewGridDimensions(state: Object) {
const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
const columns = Math.min(columnsToMaintainASquare, maxColumns);
const rows = Math.ceil(numberOfParticipants / columns);
const visibleRows = Math.min(maxColumns, rows);
const minVisibleRows = Math.min(maxColumns, rows);
return {
columns,
visibleRows
minVisibleRows,
rows
};
}