feat(filmstrip) Make filmstrip user resizable (#10884)

Make conference info and toolbar appear on top of the filmstrip
After a breakpoint, filmstrip pushes over the stage view instead of appearing on top
On user resize make tiles wider; after a breakpoint show grid view in the filmstrip
On filmstrip visibility toggle animate stage view resize
Added config for filmstrip with disableResizableFilmstrip
pull/11022/head jitsi-meet_6969
Robert Pintilii 3 years ago committed by GitHub
parent fde33b72d0
commit 2dda749b1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      config.js
  2. 2
      css/_subject.scss
  3. 4
      css/_videolayout_default.scss
  4. 26
      css/filmstrip/_vertical_filmstrip.scss
  5. 2
      css/premeeting/_premeeting-screens.scss
  6. 7
      modules/UI/videolayout/LargeVideoManager.js
  7. 1
      react/features/base/config/configWhitelist.js
  8. 18
      react/features/filmstrip/actionTypes.js
  9. 96
      react/features/filmstrip/actions.web.js
  10. 331
      react/features/filmstrip/components/web/Filmstrip.js
  11. 27
      react/features/filmstrip/components/web/Thumbnail.js
  12. 9
      react/features/filmstrip/components/web/ThumbnailWrapper.js
  13. 150
      react/features/filmstrip/components/web/styles.js
  14. 44
      react/features/filmstrip/constants.js
  15. 84
      react/features/filmstrip/functions.web.js
  16. 23
      react/features/filmstrip/middleware.web.js
  17. 43
      react/features/filmstrip/reducer.js
  18. 14
      react/features/filmstrip/subscriber.web.js
  19. 87
      react/features/large-video/components/LargeVideo.web.js
  20. 15
      react/features/video-layout/functions.js
  21. 2
      react/features/video-layout/middleware.web.js

@ -1256,6 +1256,13 @@ var config = {
// Prevent the filmstrip from autohiding when screen width is under a certain threshold
// disableFilmstripAutohiding: false,
// filmstrip: {
// // Disables user resizable filmstrip. Also, allows configuration of the filmstrip
// // (width, tiles aspect ratios) through the interfaceConfig options.
// disableResizable: false,
// }
// Specifies whether the chat emoticons are disabled or not
// disableChatSmileys: false,

@ -1,7 +1,7 @@
.subject {
color: #fff;
transition: opacity .6s ease-in-out;
z-index: $zindex3;
z-index: $toolbarZ + 2;
margin-top: 20px;
opacity: 0;

@ -78,6 +78,10 @@
#largeVideoContainer {
overflow: hidden;
text-align: center;
&.transition {
transition: width 1s, height 1s, top 1s;
}
}
#largeVideoContainer {

@ -28,7 +28,7 @@
flex-direction: column-reverse;
height: 100%;
width: 100%;
padding: ($desktopAppDragBarHeight - 5px) 5px calc(env(safe-area-inset-bottom, 0) + 10px);
padding: 0;
/**
* fixed positioning is necessary for remote menus and tooltips to pop
* out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
@ -40,6 +40,10 @@
right: 0;
z-index: $filmstripVideosZ;
&.no-vertical-padding {
padding: 0;
}
/**
* Hide videos by making them slight to the right.
*/
@ -58,7 +62,10 @@
&#remoteVideos {
border: $thumbnailsBorder solid transparent;
padding-left: 0;
border-left: 0;
width: 100%;
height: 100%;
justify-content: center;
}
}
@ -67,11 +74,12 @@
*/
#filmstripLocalVideo {
align-self: initial;
bottom: 5px;
margin-bottom: 5px;
display: flex;
flex-direction: column-reverse;
height: auto;
justify-content: flex-start;
width: 100%;
#filmstripLocalVideoThumbnail {
width: calc(100% - 15px);
@ -100,15 +108,27 @@
flex-grow: 1;
}
.resizable-filmstrip #remoteVideos .videocontainer {
border-left: 0;
margin: 0;
}
&.reduce-height {
height: calc(100% - calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight}));
}
.remote-videos {
display: flex;
transition: height .3s ease-in;
overscroll-behavior: contain;
&.height-transition {
transition: height .3s ease-in;
}
&.vertical-grid-margin > div {
margin-right: $scrollHeight;
}
& > div {
position: absolute;
transition: opacity 1s;

@ -7,7 +7,7 @@
position: absolute;
right: 0;
top: 0;
z-index: $toolbarZ + 1;
z-index: $toolbarZ + 2;
.action-btn {
border-radius: 6px;

@ -26,6 +26,7 @@ import {
isTrackStreamingStatusInactive,
isTrackStreamingStatusInterrupted
} from '../../../react/features/connection-indicator/functions';
import { FILMSTRIP_BREAKPOINT, isFilmstripResizable } from '../../../react/features/filmstrip';
import {
updateKnownLargeVideoResolution
} from '../../../react/features/large-video/actions';
@ -401,7 +402,9 @@ export default class LargeVideoManager {
let widthToUse = this.preferredWidth || window.innerWidth;
const state = APP.store.getState();
const { isOpen } = state['features/chat'];
const { width: filmstripWidth, visible } = state['features/filmstrip'];
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
const resizableFilmstrip = isFilmstripResizable(state);
if (isParticipantsPaneOpen) {
widthToUse -= theme.participantsPaneWidth;
@ -415,6 +418,10 @@ export default class LargeVideoManager {
widthToUse -= CHAT_SIZE;
}
if (resizableFilmstrip && visible && filmstripWidth.current >= FILMSTRIP_BREAKPOINT) {
widthToUse -= filmstripWidth.current;
}
this.width = widthToUse;
this.height = this.preferredHeight || window.innerHeight;
}

@ -154,6 +154,7 @@ export default [
'failICE',
'feedbackPercentage',
'fileRecordingsEnabled',
'filmstrip',
'firefox_fake_device',
'forceJVB121Ratio',
'forceTurnRelay',

@ -91,3 +91,21 @@ export const SET_VOLUME = 'SET_VOLUME';
* }
*/
export const SET_VISIBLE_REMOTE_PARTICIPANTS = 'SET_VISIBLE_REMOTE_PARTICIPANTS';
/**
* The type of action which sets the width for the vertical filmstrip.
* {
* type: SET_FILMSTRIP_WIDTH,
* width: number
* }
*/
export const SET_FILMSTRIP_WIDTH = 'SET_FILMSTRIP_WIDTH';
/**
* The type of action which sets the width for the vertical filmstrip (user resized).
* {
* type: SET_USER_FILMSTRIP_WIDTH,
* width: number
* }
*/
export const SET_USER_FILMSTRIP_WIDTH = 'SET_USER_FILMSTRIP_WIDTH';

@ -3,26 +3,32 @@ import type { Dispatch } from 'redux';
import { getLocalParticipant, getParticipantById, pinParticipant } from '../base/participants';
import { shouldHideSelfView } from '../base/settings/functions.any';
import { getTileViewGridDimensions } from '../video-layout';
import {
SET_FILMSTRIP_WIDTH,
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS,
SET_USER_FILMSTRIP_WIDTH,
SET_VERTICAL_VIEW_DIMENSIONS,
SET_VOLUME
} from './actionTypes';
import {
HORIZONTAL_FILMSTRIP_MARGIN,
SCROLL_SIZE,
STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
VERTICAL_FILMSTRIP_VERTICAL_MARGIN
} from './constants';
import {
calculateThumbnailSizeForHorizontalView,
calculateThumbnailSizeForTileView,
calculateThumbnailSizeForVerticalView
calculateThumbnailSizeForVerticalView,
calculateThumbnailSizeForResizableVerticalView,
isFilmstripResizable,
showGridInVerticalView
} from './functions';
export * from './actions.any';
@ -80,21 +86,65 @@ export function setVerticalViewDimensions() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
const { width: filmstripWidth } = state['features/filmstrip'];
const disableSelfView = shouldHideSelfView(state);
const thumbnails = calculateThumbnailSizeForVerticalView(clientWidth);
const resizableFilmstrip = isFilmstripResizable(state);
const _verticalViewGrid = showGridInVerticalView(state);
let gridView = {};
let thumbnails = {};
let filmstripDimensions = {};
// grid view in the vertical filmstrip
if (_verticalViewGrid) {
const dimensions = getTileViewGridDimensions(state, filmstripWidth.current);
const {
height,
width
} = calculateThumbnailSizeForTileView({
...dimensions,
clientWidth: filmstripWidth.current,
clientHeight,
disableResponsiveTiles: false,
disableTileEnlargement: false,
isVerticalFilmstrip: true
});
const { columns, rows } = dimensions;
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
const widthOfFilmstrip = (columns * (TILE_HORIZONTAL_MARGIN + width)) + (hasScroll ? SCROLL_SIZE : 0);
const filmstripHeight = Math.min(clientHeight, thumbnailsTotalHeight);
gridView = {
gridDimensions: dimensions,
thumbnailSize: {
height,
width
}
};
filmstripDimensions = {
height: filmstripHeight,
width: widthOfFilmstrip
};
} else {
thumbnails = resizableFilmstrip
? calculateThumbnailSizeForResizableVerticalView(clientWidth, filmstripWidth.current)
: calculateThumbnailSizeForVerticalView(clientWidth);
}
dispatch({
type: SET_VERTICAL_VIEW_DIMENSIONS,
dimensions: {
...thumbnails,
remoteVideosContainer: {
remoteVideosContainer: _verticalViewGrid ? filmstripDimensions : {
width: thumbnails?.local?.width
+ TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER + SCROLL_SIZE,
+ TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN + SCROLL_SIZE,
height: clientHeight - (disableSelfView ? 0 : thumbnails?.local?.height)
- VERTICAL_FILMSTRIP_VERTICAL_MARGIN
}
},
gridView
}
});
};
}
@ -163,3 +213,35 @@ export function setVolume(participantId: string, volume: number) {
volume
};
}
/**
* Sets the filmstrip's width.
*
* @param {number} width - The new width of the filmstrip.
* @returns {{
* type: SET_FILMSTRIP_WIDTH,
* width: number
* }}
*/
export function setFilmstripWidth(width: number) {
return {
type: SET_FILMSTRIP_WIDTH,
width
};
}
/**
* Sets the filmstrip's width and the user preferred width.
*
* @param {number} width - The new width of the filmstrip.
* @returns {{
* type: SET_USER_FILMSTRIP_WIDTH,
* width: number
* }}
*/
export function setUserFilmstripWidth(width: number) {
return {
type: SET_USER_FILMSTRIP_WIDTH,
width
};
}

@ -2,6 +2,7 @@
import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import _ from 'lodash';
import React, { PureComponent } from 'react';
import { FixedSizeList, FixedSizeGrid } from 'react-window';
import type { Dispatch } from 'redux';
@ -20,19 +21,28 @@ import { shouldHideSelfView } from '../../../base/settings/functions.any';
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 { setFilmstripVisible, setVisibleRemoteParticipants, setUserFilmstripWidth } from '../../actions';
import {
ASPECT_RATIO_BREAKPOINT,
DEFAULT_FILMSTRIP_WIDTH,
FILMSTRIP_BREAKPOINT,
FILMSTRIP_BREAKPOINT_OFFSET,
MIN_STAGE_VIEW_WIDTH,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
TOOLBAR_HEIGHT,
TOOLBAR_HEIGHT_MOBILE
} from '../../constants';
import { shouldRemoteVideosBeVisible } from '../../functions';
import {
isFilmstripResizable,
shouldRemoteVideosBeVisible,
showGridInVerticalView
} from '../../functions';
import AudioTracksContainer from './AudioTracksContainer';
import Thumbnail from './Thumbnail';
import ThumbnailWrapper from './ThumbnailWrapper';
import { styles } from './styles';
declare var APP: Object;
declare var interfaceConfig: Object;
@ -82,11 +92,21 @@ type Props = {
*/
_isFilmstripButtonEnabled: boolean,
/**
* Whether or not the toolbox is displayed.
*/
_isToolboxVisible: Boolean,
/**
* Whether or not the current layout is vertical filmstrip.
*/
_isVerticalFilmstrip: boolean,
/**
* The maximum width of the vertical filmstrip.
*/
_maxFilmstripWidth: number,
/**
* The participants in the call.
*/
@ -97,6 +117,11 @@ type Props = {
*/
_remoteParticipantsLength: number,
/**
* Whether or not the filmstrip should be user-resizable.
*/
_resizableFilmstrip: boolean,
/**
* The number of rows in tile view.
*/
@ -117,6 +142,16 @@ type Props = {
*/
_thumbnailsReordered: Boolean,
/**
* The width of the vertical filmstrip (user resized).
*/
_verticalFilmstripWidth: ?number,
/**
* Whether or not the vertical filmstrip should be displayed as grid.
*/
_verticalViewGrid: boolean,
/**
* Additional CSS class names to add to the container of all the thumbnails.
*/
@ -127,11 +162,6 @@ type Props = {
*/
_visible: boolean,
/**
* Whether or not the toolbox is displayed.
*/
_isToolboxVisible: Boolean,
/**
* An object containing the CSS classes.
*/
@ -148,83 +178,23 @@ type Props = {
t: Function
};
/**
* Creates the styles for the component.
*
* @param {Object} theme - The current theme.
* @returns {Object}
*/
const styles = theme => {
return {
toggleFilmstripContainer: {
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0, 0, 0, .6)',
width: '32px',
height: '24px',
position: 'absolute',
borderRadius: '4px',
top: 'calc(-24px - 2px)',
left: 'calc(50% - 16px)',
opacity: 0,
transition: 'opacity .3s'
},
toggleFilmstripButton: {
fontSize: '14px',
lineHeight: 1.2,
textAlign: 'center',
background: 'transparent',
height: 'auto',
width: '100%',
padding: 0,
margin: 0,
border: 'none',
'-webkit-appearance': 'none',
'& svg': {
fill: theme.palette.icon02
}
},
toggleVerticalFilmstripContainer: {
transform: 'rotate(-90deg)',
left: 'calc(-24px - 2px - 5px)',
top: 'calc(50% - 16px)'
},
filmstrip: {
transition: 'background .2s ease-in-out, right 1s, bottom 1s, height .3s ease-in',
right: 0,
bottom: 0,
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, .6)',
'& .toggleFilmstripContainer': {
opacity: 1
}
},
type State = {
'.horizontal-filmstrip &.hidden': {
bottom: '-50px',
/**
* Whether or not the mouse is pressed.
*/
isMouseDown: boolean,
'&:hover': {
backgroundColor: 'transparent'
}
},
/**
* Initial mouse position on drag handle mouse down.
*/
mousePosition: ?number,
'&.hidden': {
'& .toggleFilmstripContainer': {
opacity: 1
}
}
}
};
};
/**
* Initial filmstrip width on drag handle mouse down.
*/
dragFilmstripWidth: ?number
}
/**
* Implements a React {@link Component} which represents the filmstrip on
@ -232,7 +202,9 @@ const styles = theme => {
*
* @augments Component
*/
class Filmstrip extends PureComponent <Props> {
class Filmstrip extends PureComponent <Props, State> {
_throttledResize: Function;
/**
* Initializes a new {@code Filmstrip} instance.
@ -243,6 +215,12 @@ class Filmstrip extends PureComponent <Props> {
constructor(props: Props) {
super(props);
this.state = {
isMouseDown: false,
mousePosition: null,
dragFilmstripWidth: null
};
// Bind event handlers so they are only bound once for every instance.
this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
@ -252,6 +230,17 @@ class Filmstrip extends PureComponent <Props> {
this._onGridItemsRendered = this._onGridItemsRendered.bind(this);
this._onListItemsRendered = this._onListItemsRendered.bind(this);
this._onToggleButtonTouch = this._onToggleButtonTouch.bind(this);
this._onDragHandleMouseDown = this._onDragHandleMouseDown.bind(this);
this._onDragMouseUp = this._onDragMouseUp.bind(this);
this._onFilmstripResize = this._onFilmstripResize.bind(this);
this._throttledResize = _.throttle(
this._onFilmstripResize,
50,
{
leading: true,
trailing: false
});
}
/**
@ -266,6 +255,8 @@ class Filmstrip extends PureComponent <Props> {
this._onShortcutToggleFilmstrip,
'keyboardShortcuts.toggleFilmstrip'
);
document.addEventListener('mouseup', this._onDragMouseUp);
document.addEventListener('mousemove', this._throttledResize);
}
/**
@ -275,6 +266,8 @@ class Filmstrip extends PureComponent <Props> {
*/
componentWillUnmount() {
APP.keyboardshortcut.unregisterShortcut('F');
document.removeEventListener('mouseup', this._onDragMouseUp);
document.removeEventListener('mousemove', this._throttledResize);
}
/**
@ -285,17 +278,32 @@ class Filmstrip extends PureComponent <Props> {
*/
render() {
const filmstripStyle = { };
const { _currentLayout, _disableSelfView, classes, _visible } = this.props;
const {
_currentLayout,
_disableSelfView,
_resizableFilmstrip,
_verticalFilmstripWidth,
_visible,
_verticalViewGrid,
classes
} = this.props;
const { isMouseDown } = this.state;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
let maxWidth;
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;
maxWidth = _resizableFilmstrip
? _verticalFilmstripWidth || DEFAULT_FILMSTRIP_WIDTH
: interfaceConfig.FILM_STRIP_MAX_HEIGHT || DEFAULT_FILMSTRIP_WIDTH;
// Adding 4px for the border-right and margin-right.
// On non-resizable filmstrip add 4px for the left margin and border.
// Also adding 7px for the scrollbar. Also adding 9px for the drag handle.
filmstripStyle.maxWidth = maxWidth + (_verticalViewGrid ? 0 : 11) + (_resizableFilmstrip ? 9 : 4);
if (!_visible) {
filmstripStyle.right = `-${filmstripStyle.maxWidth + 2}px`;
filmstripStyle.right = `-${filmstripStyle.maxWidth}px`;
}
break;
}
@ -306,37 +314,113 @@ class Filmstrip extends PureComponent <Props> {
toolbar = this._renderToggleButton();
}
const filmstrip = (<>
<div
className = { clsx(this.props._videosClassName,
!tileViewActive && !_resizableFilmstrip && 'filmstrip-hover') }
id = 'remoteVideos'>
{!_disableSelfView && !_verticalViewGrid && (
<div
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'>
<div id = 'filmstripLocalVideoThumbnail'>
{
!tileViewActive && <Thumbnail
key = 'local' />
}
</div>
</div>
)}
{
this._renderRemoteParticipants()
}
</div>
</>);
return (
<div
className = { clsx('filmstrip',
this.props._className,
classes.filmstrip) }
classes.filmstrip,
_verticalViewGrid && 'no-vertical-padding',
_verticalFilmstripWidth + FILMSTRIP_BREAKPOINT_OFFSET >= FILMSTRIP_BREAKPOINT
&& classes.filmstripBackground) }
style = { filmstripStyle }>
{ toolbar }
<div
className = { this.props._videosClassName }
id = 'remoteVideos'>
{!_disableSelfView && (
{_resizableFilmstrip
? <div className = { clsx('resizable-filmstrip', classes.resizableFilmstripContainer) }>
<div
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'>
<div id = 'filmstripLocalVideoThumbnail'>
{
!tileViewActive && <Thumbnail
key = 'local' />
}
</div>
className = { clsx('dragHandleContainer',
classes.dragHandleContainer,
isMouseDown && 'visible')
}
onMouseDown = { this._onDragHandleMouseDown }>
<div className = { clsx(classes.dragHandle, 'dragHandle') } />
</div>
)}
{
this._renderRemoteParticipants()
}
</div>
{filmstrip}
</div>
: filmstrip
}
<AudioTracksContainer />
</div>
);
}
_onDragHandleMouseDown: (MouseEvent) => void;
/**
* Handles mouse down on the drag handle.
*
* @param {MouseEvent} e - The mouse down event.
* @returns {void}
*/
_onDragHandleMouseDown(e) {
this.setState({
isMouseDown: true,
mousePosition: e.clientX,
dragFilmstripWidth: this.props._verticalFilmstripWidth || DEFAULT_FILMSTRIP_WIDTH
});
}
_onDragMouseUp: () => void;
/**
* Drag handle mouse up handler.
*
* @returns {void}
*/
_onDragMouseUp() {
if (this.state.isMouseDown) {
this.setState({
isMouseDown: false
});
}
}
_onFilmstripResize: (MouseEvent) => void;
/**
* Handles drag handle mouse move.
*
* @param {MouseEvent} e - The mousemove event.
* @returns {void}
*/
_onFilmstripResize(e) {
if (this.state.isMouseDown) {
const { dispatch, _verticalFilmstripWidth, _maxFilmstripWidth } = this.props;
const { dragFilmstripWidth, mousePosition } = this.state;
const diff = mousePosition - e.clientX;
const width = Math.max(
Math.min(dragFilmstripWidth + diff, _maxFilmstripWidth),
DEFAULT_FILMSTRIP_WIDTH
);
if (width !== _verticalFilmstripWidth) {
dispatch(setUserFilmstripWidth(width));
}
}
}
/**
* Calculates the start and stop indices based on whether the thumbnails need to be reordered in the filmstrip.
*
@ -480,7 +564,8 @@ class Filmstrip extends PureComponent <Props> {
_remoteParticipantsLength,
_rows,
_thumbnailHeight,
_thumbnailWidth
_thumbnailWidth,
_verticalViewGrid
} = this.props;
if (!_thumbnailWidth || isNaN(_thumbnailWidth) || !_thumbnailHeight
@ -489,7 +574,7 @@ class Filmstrip extends PureComponent <Props> {
return null;
}
if (_currentLayout === LAYOUTS.TILE_VIEW) {
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid) {
return (
<FixedSizeGrid
className = 'filmstrip__videos remote-videos'
@ -514,7 +599,7 @@ class Filmstrip extends PureComponent <Props> {
const props = {
itemCount: _remoteParticipantsLength,
className: 'filmstrip__videos remote-videos',
className: 'filmstrip__videos remote-videos height-transition',
height: _filmstripHeight,
itemKey: this._listItemKey,
itemSize: 0,
@ -668,18 +753,21 @@ function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const { visible, remoteParticipants } = state['features/filmstrip'];
const { visible, remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const {
gridDimensions = {},
gridDimensions: dimensions = {},
filmstripHeight,
filmstripWidth,
thumbnailSize: tileViewThumbnailSize
} = state['features/filmstrip'].tileViewDimensions;
const _currentLayout = getCurrentLayout(state);
const disableSelfView = shouldHideSelfView(state);
const _resizableFilmstrip = isFilmstripResizable(state);
const _verticalViewGrid = showGridInVerticalView(state);
let gridDimensions = dimensions;
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const availableSpace = clientHeight - filmstripHeight;
@ -703,7 +791,7 @@ function _mapStateToProps(state) {
isMobileBrowser() || _currentLayout !== LAYOUTS.VERTICAL_FILMSTRIP_VIEW);
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
const className = `${remoteVideosVisible || _verticalViewGrid ? '' : 'hide-videos'} ${
shouldReduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${visible ? '' : 'hidden'}`.trim();
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
@ -715,11 +803,18 @@ function _mapStateToProps(state) {
remoteFilmstripWidth = filmstripWidth;
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
const { remote, remoteVideosContainer } = state['features/filmstrip'].verticalViewDimensions;
const { remote, remoteVideosContainer, gridView } = state['features/filmstrip'].verticalViewDimensions;
_thumbnailSize = remote;
remoteFilmstripHeight = remoteVideosContainer?.height - (shouldReduceHeight ? TOOLBAR_HEIGHT : 0);
remoteFilmstripHeight = remoteVideosContainer?.height - (!_verticalViewGrid && shouldReduceHeight
? TOOLBAR_HEIGHT : 0);
remoteFilmstripWidth = remoteVideosContainer?.width;
if (_verticalViewGrid) {
gridDimensions = gridView.gridDimensions;
_thumbnailSize = gridView.thumbnailSize;
} else {
_thumbnailSize = remote;
}
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
@ -741,16 +836,20 @@ function _mapStateToProps(state) {
_filmstripWidth: remoteFilmstripWidth,
_iAmRecorder: Boolean(iAmRecorder),
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_isToolboxVisible: isToolboxVisible(state),
_isVerticalFilmstrip: _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW,
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,
_resizableFilmstrip,
_rows: gridDimensions.rows,
_thumbnailWidth: _thumbnailSize?.width,
_thumbnailHeight: _thumbnailSize?.height,
_thumbnailsReordered: enableThumbnailReordering,
_verticalFilmstripWidth: verticalFilmstripWidth.current,
_videosClassName: videosClassName,
_visible: visible,
_isToolboxVisible: isToolboxVisible(state),
_isVerticalFilmstrip: _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
_verticalViewGrid
};
}

@ -28,10 +28,15 @@ import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
DISPLAY_MODE_TO_CLASS_NAME,
DISPLAY_VIDEO,
VIDEO_TEST_EVENTS,
SHOW_TOOLBAR_CONTEXT_MENU_AFTER
SHOW_TOOLBAR_CONTEXT_MENU_AFTER,
VIDEO_TEST_EVENTS
} from '../../constants';
import { isVideoPlayable, computeDisplayModeFromInput, getDisplayModeInput } from '../../functions';
import {
computeDisplayModeFromInput,
getDisplayModeInput,
isVideoPlayable,
showGridInVerticalView
} from '../../functions';
import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
@ -480,7 +485,6 @@ class Thumbnail extends Component<Props, State> {
style
} = this.props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
const track = jitsiVideoTrack?.track;
@ -949,19 +953,30 @@ function _mapStateToProps(state, ownProps): Object {
},
verticalViewDimensions = {
local: {},
remote: {}
remote: {},
gridView: {}
}
} = state['features/filmstrip'];
const _verticalViewGrid = showGridInVerticalView(state);
const { local, remote }
= _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
? verticalViewDimensions : horizontalViewDimensions;
const { width, height } = isLocal ? local : remote;
const { width, height } = (isLocal ? local : remote) ?? {};
size = {
_width: width,
_height: height
};
if (_verticalViewGrid) {
const { width: _width, height: _height } = verticalViewDimensions.gridView.thumbnailSize;
size = {
_width,
_height
};
}
_isMobilePortrait = _isMobile && state['features/base/responsive-ui'].aspectRatio === ASPECT_RATIO_NARROW;
break;

@ -5,6 +5,7 @@ import { shouldComponentUpdate } from 'react-window';
import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { showGridInVerticalView } from '../../functions';
import Thumbnail from './Thumbnail';
@ -120,10 +121,14 @@ function _mapStateToProps(state, ownProps) {
const { testing = {} } = state['features/base/config'];
const disableSelfView = shouldHideSelfView(state);
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const _verticalViewGrid = showGridInVerticalView(state);
if (_currentLayout === LAYOUTS.TILE_VIEW) {
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid) {
const { columnIndex, rowIndex } = ownProps;
const { gridDimensions = {}, thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
const { gridDimensions: dimensions = {}, thumbnailSize: size } = state['features/filmstrip'].tileViewDimensions;
const { gridView } = state['features/filmstrip'].verticalViewDimensions;
const gridDimensions = _verticalViewGrid ? gridView.gridDimensions : dimensions;
const thumbnailSize = _verticalViewGrid ? gridView.thumbnailSize : size;
const { columns, rows } = gridDimensions;
const index = (rowIndex * columns) + columnIndex;
let horizontalOffset;

@ -0,0 +1,150 @@
const BACKGROUND_COLOR = 'rgba(51, 51, 51, .5)';
/**
* Creates the styles for the component.
*
* @param {Object} theme - The current theme.
* @returns {Object}
*/
export const styles = theme => {
return {
toggleFilmstripContainer: {
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: BACKGROUND_COLOR,
width: '32px',
height: '24px',
position: 'absolute',
borderRadius: '4px',
top: 'calc(-24px - 3px)',
left: 'calc(50% - 16px)',
opacity: 0,
transition: 'opacity .3s',
'&:hover': {
backgroundColor: theme.palette.ui02
}
},
toggleFilmstripButton: {
fontSize: '14px',
lineHeight: 1.2,
textAlign: 'center',
background: 'transparent',
height: 'auto',
width: '100%',
padding: 0,
margin: 0,
border: 'none',
'-webkit-appearance': 'none',
'& svg': {
fill: theme.palette.icon01
}
},
toggleVerticalFilmstripContainer: {
transform: 'rotate(-90deg)',
left: 'calc(-24px - 3px - 4px)',
top: 'calc(50% - 12px)'
},
filmstrip: {
transition: 'background .2s ease-in-out, right 1s, bottom 1s, height .3s ease-in',
right: 0,
bottom: 0,
'&:hover': {
'& .resizable-filmstrip': {
backgroundColor: BACKGROUND_COLOR
},
'& .filmstrip-hover': {
backgroundColor: BACKGROUND_COLOR
},
'& .toggleFilmstripContainer': {
opacity: 1
},
'& .dragHandleContainer': {
visibility: 'visible'
}
},
'.horizontal-filmstrip &.hidden': {
bottom: '-50px',
'&:hover': {
backgroundColor: 'transparent'
}
},
'&.hidden': {
'& .toggleFilmstripContainer': {
opacity: 1
}
}
},
filmstripBackground: {
backgroundColor: theme.palette.uiBackground,
'&:hover': {
backgroundColor: theme.palette.uiBackground
}
},
resizableFilmstripContainer: {
display: 'flex',
position: 'relative',
flexDirection: 'row',
alignItems: 'center',
height: '100%',
width: '100%',
transition: 'background .2s ease-in-out',
'& .avatar-container': {
maxWidth: 'initial',
maxHeight: 'initial'
}
},
dragHandleContainer: {
height: '100%',
width: '9px',
backgroundColor: 'transparent',
position: 'relative',
cursor: 'col-resize',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
visibility: 'hidden',
'&:hover': {
'& .dragHandle': {
backgroundColor: theme.palette.icon01
}
},
'&.visible': {
visibility: 'visible',
'& .dragHandle': {
backgroundColor: theme.palette.icon01
}
}
},
dragHandle: {
backgroundColor: theme.palette.icon02,
height: '100px',
width: '3px',
borderRadius: '1px'
}
};
};

@ -137,6 +137,14 @@ export const TILE_VERTICAL_MARGIN = 4;
*/
export const TILE_HORIZONTAL_MARGIN = 4;
/**
* The horizontal margin of a vertical filmstrip tile container.
*
* @type {number}
*/
export const TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN = 2;
/**
* The vertical margin of the tile grid container.
*
@ -189,7 +197,7 @@ export const SCROLL_SIZE = 7;
*
* @type {number}
*/
export const VERTICAL_FILMSTRIP_VERTICAL_MARGIN = 60;
export const VERTICAL_FILMSTRIP_VERTICAL_MARGIN = 26;
/**
* The min horizontal space between the thumbnails container and the edges of the window.
@ -242,3 +250,37 @@ export const INDICATORS_TOOLTIP_POSITION = {
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'left',
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'top'
};
/**
* The default (and minimum) width for the vertical filmstrip (user resizable).
*/
export const DEFAULT_FILMSTRIP_WIDTH = 120;
/**
* The width of the filmstrip at which it no longer goes above the stage view, but it pushes it.
*/
export const FILMSTRIP_BREAKPOINT = 180;
/**
* The width of the filmstrip at which the display mode changes from column to grid.
*/
export const FILMSTRIP_GRID_BREAKPOINT = 300;
/**
* How much before the breakpoint should we display the background.
* (We display the opaque background before we resize the stage view to make sure
* the resize is not visible behind the filmstrip).
*/
export const FILMSTRIP_BREAKPOINT_OFFSET = 5;
/**
* The minimum width for the stage view
* (used to determine the maximum width of the user-resizable vertical filmstrip).
*/
export const MIN_STAGE_VIEW_WIDTH = 800;
/**
* Horizontal margin used for the vertical filmstrip.
*/
export const VERTICAL_VIEW_HORIZONTAL_MARGIN = VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
+ SCROLL_SIZE + TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER;

@ -1,6 +1,7 @@
// @flow
import { getSourceNameSignalingFeatureFlag } from '../base/config';
import { isMobileBrowser } from '../base/environment/utils';
import { MEDIA_TYPE } from '../base/media';
import {
getLocalParticipant,
@ -16,25 +17,26 @@ import {
isRemoteTrackMuted
} from '../base/tracks/functions';
import { isTrackStreamingStatusActive, isParticipantConnectionStatusActive } from '../connection-indicator/functions';
import { LAYOUTS } from '../video-layout';
import { getCurrentLayout, LAYOUTS } from '../video-layout';
import {
ASPECT_RATIO_BREAKPOINT,
DEFAULT_FILMSTRIP_WIDTH,
DISPLAY_AVATAR,
DISPLAY_VIDEO,
FILMSTRIP_GRID_BREAKPOINT,
INDICATORS_TOOLTIP_POSITION,
SCROLL_SIZE,
SQUARE_TILE_ASPECT_RATIO,
STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
TILE_ASPECT_RATIO,
TILE_HORIZONTAL_MARGIN,
TILE_MIN_HEIGHT_LARGE,
TILE_MIN_HEIGHT_SMALL,
TILE_PORTRAIT_ASPECT_RATIO,
TILE_VERTICAL_MARGIN,
TILE_VIEW_GRID_HORIZONTAL_MARGIN,
TILE_VIEW_GRID_VERTICAL_MARGIN,
VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN,
TILE_MIN_HEIGHT_LARGE,
TILE_MIN_HEIGHT_SMALL,
TILE_PORTRAIT_ASPECT_RATIO
VERTICAL_VIEW_HORIZONTAL_MARGIN
} from './constants';
export * from './functions.any';
@ -139,7 +141,8 @@ export function isVideoPlayable(stateful: Object | Function, id: String) {
*/
export function calculateThumbnailSizeForHorizontalView(clientHeight: number = 0) {
const topBottomMargin = 15;
const availableHeight = Math.min(clientHeight, (interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120) + topBottomMargin);
const availableHeight = Math.min(clientHeight,
(interfaceConfig.FILM_STRIP_MAX_HEIGHT || DEFAULT_FILMSTRIP_WIDTH) + topBottomMargin);
const height = availableHeight - topBottomMargin;
return {
@ -161,12 +164,9 @@ export function calculateThumbnailSizeForHorizontalView(clientHeight: number = 0
* @returns {{local: {height, width}, remote: {height, width}}}
*/
export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0) {
const horizontalMargin
= VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN + SCROLL_SIZE
+ TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER;
const availableWidth = Math.min(
Math.max(clientWidth - horizontalMargin, 0),
interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120);
Math.max(clientWidth - VERTICAL_VIEW_HORIZONTAL_MARGIN, 0),
interfaceConfig.FILM_STRIP_MAX_HEIGHT || DEFAULT_FILMSTRIP_WIDTH);
return {
local: {
@ -180,6 +180,31 @@ export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0) {
};
}
/**
* Calculates the size for thumbnails when in vertical view layout
* and the filmstrip is resizable.
*
* @param {number} clientWidth - The height of the app window.
* @param {number} filmstripWidth - The width of the filmstrip.
* @returns {{local: {height, width}, remote: {height, width}}}
*/
export function calculateThumbnailSizeForResizableVerticalView(clientWidth: number = 0, filmstripWidth: number = 0) {
const availableWidth = Math.min(
Math.max(clientWidth - VERTICAL_VIEW_HORIZONTAL_MARGIN, 0),
filmstripWidth || DEFAULT_FILMSTRIP_WIDTH);
return {
local: {
height: DEFAULT_FILMSTRIP_WIDTH,
width: availableWidth
},
remote: {
height: DEFAULT_FILMSTRIP_WIDTH,
width: availableWidth
}
};
}
/**
* Calculates the size for thumbnails when in tile view layout.
*
@ -193,7 +218,8 @@ export function calculateThumbnailSizeForTileView({
clientWidth,
clientHeight,
disableResponsiveTiles,
disableTileEnlargement
disableTileEnlargement,
isVerticalFilmstrip = false
}: Object) {
let aspectRatio = TILE_ASPECT_RATIO;
@ -202,7 +228,8 @@ export function calculateThumbnailSizeForTileView({
}
const minHeight = clientWidth < ASPECT_RATIO_BREAKPOINT ? TILE_MIN_HEIGHT_SMALL : TILE_MIN_HEIGHT_LARGE;
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN) - TILE_VIEW_GRID_HORIZONTAL_MARGIN;
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
- (isVerticalFilmstrip ? 0 : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
const initialWidth = viewWidth / columns;
const initialHeight = viewHeight / minVisibleRows;
@ -285,7 +312,7 @@ export function getVerticalFilmstripVisibleAreaWidth() {
// TODO: Check if we can remove the left margins and paddings from the CSS.
// FIXME: This function is used to calculate the size of the large video, etherpad or shared video. Once everything
// is reactified this calculation will need to move to the corresponding components.
const filmstripMaxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120) + 18;
const filmstripMaxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || DEFAULT_FILMSTRIP_WIDTH) + 18;
return Math.min(filmstripMaxWidth, window.innerWidth);
}
@ -365,3 +392,30 @@ export function getDisplayModeInput(props: Object, state: Object) {
export function getIndicatorsTooltipPosition(currentLayout: string) {
return INDICATORS_TOOLTIP_POSITION[currentLayout] || 'top';
}
/**
* Returns whether or not the filmstrip is resizable.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function isFilmstripResizable(state: Object) {
const { filmstrip } = state['features/base/config'];
const _currentLayout = getCurrentLayout(state);
return !filmstrip?.disableResizable && !isMobileBrowser()
&& _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
}
/**
* Whether or not grid should be displayed in the vertical filmstrip.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function showGridInVerticalView(state) {
const resizableFilmstrip = isFilmstripResizable(state);
const { width } = state['features/filmstrip'];
return resizableFilmstrip && ((width.current ?? 0) > FILMSTRIP_GRID_BREAKPOINT);
}

@ -10,12 +10,16 @@ import {
LAYOUTS
} from '../video-layout';
import { SET_USER_FILMSTRIP_WIDTH } from './actionTypes';
import {
setFilmstripWidth,
setHorizontalViewDimensions,
setTileViewDimensions,
setVerticalViewDimensions
} from './actions';
import { DEFAULT_FILMSTRIP_WIDTH, MIN_STAGE_VIEW_WIDTH } from './constants';
import { updateRemoteParticipants, updateRemoteParticipantsOnLeave } from './functions';
import { isFilmstripResizable } from './functions.web';
import './subscriber';
/**
@ -53,6 +57,22 @@ MiddlewareRegistry.register(store => next => action => {
store.dispatch(setVerticalViewDimensions());
break;
}
if (isFilmstripResizable(state)) {
const { width: filmstripWidth } = state['features/filmstrip'];
const { clientWidth } = action;
let width;
if (filmstripWidth.current > clientWidth - MIN_STAGE_VIEW_WIDTH) {
width = Math.max(clientWidth - MIN_STAGE_VIEW_WIDTH, DEFAULT_FILMSTRIP_WIDTH);
} else {
width = Math.min(clientWidth - MIN_STAGE_VIEW_WIDTH, filmstripWidth.userSet);
}
if (width !== filmstripWidth.current) {
store.dispatch(setFilmstripWidth(width));
}
}
break;
}
case PARTICIPANT_JOINED: {
@ -66,6 +86,9 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
}
case SET_USER_FILMSTRIP_WIDTH: {
VideoLayout.refreshLayout();
}
}
return result;

@ -6,9 +6,11 @@ import { ReducerRegistry } from '../base/redux';
import {
SET_FILMSTRIP_ENABLED,
SET_FILMSTRIP_VISIBLE,
SET_FILMSTRIP_WIDTH,
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_REMOTE_PARTICIPANTS,
SET_TILE_VIEW_DIMENSIONS,
SET_USER_FILMSTRIP_WIDTH,
SET_VERTICAL_VIEW_DIMENSIONS,
SET_VISIBLE_REMOTE_PARTICIPANTS,
SET_VOLUME
@ -92,7 +94,26 @@ const DEFAULT_STATE = {
* @public
* @type {Set<string>}
*/
visibleRemoteParticipants: new Set()
visibleRemoteParticipants: new Set(),
/**
* The width of the resizable filmstrip.
*
* @public
* @type {Object}
*/
width: {
/**
* Current width. Affected by: user filmstrip resize,
* window resize, panels open/ close.
*/
current: null,
/**
* Width set by user resize. Used as the preferred width.
*/
userSet: null
}
};
ReducerRegistry.register(
@ -167,6 +188,26 @@ ReducerRegistry.register(
...state
};
}
case SET_FILMSTRIP_WIDTH: {
return {
...state,
width: {
...state.width,
current: action.width
}
};
}
case SET_USER_FILMSTRIP_WIDTH: {
const { width } = action;
return {
...state,
width: {
current: width,
userSet: width
}
};
}
}
return state;

@ -21,6 +21,7 @@ import {
SINGLE_COLUMN_BREAKPOINT,
TWO_COLUMN_BREAKPOINT
} from './constants';
import { isFilmstripResizable } from './functions.web';
import './subscriber.any';
@ -36,6 +37,7 @@ StateListenerRegistry.register(
},
/* listener */ (currentState, store) => {
const state = store.getState();
const resizableFilmstrip = isFilmstripResizable(state);
if (shouldDisplayTileView(state)) {
const gridDimensions = getTileViewGridDimensions(state);
@ -45,6 +47,9 @@ StateListenerRegistry.register(
store.dispatch(setTileViewDimensions(gridDimensions));
}
}
if (resizableFilmstrip) {
store.dispatch(setVerticalViewDimensions());
}
}, {
deepEquals: true
});
@ -170,3 +175,12 @@ StateListenerRegistry.register(
store.dispatch(setTileViewDimensions(gridDimensions));
}
});
/**
* Listens for changes in the filmstrip width to determine the size of the tiles.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/filmstrip'].width?.current,
/* listener */(_, store) => {
store.dispatch(setVerticalViewDimensions());
});

@ -2,9 +2,11 @@
import React, { Component } from 'react';
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
import { Watermarks } from '../../base/react';
import { connect } from '../../base/redux';
import { setColorAlpha } from '../../base/util';
import { FILMSTRIP_BREAKPOINT, isFilmstripResizable } from '../../filmstrip';
import { SharedVideo } from '../../shared-video/components/web';
import { Captions } from '../../subtitles/';
import { setTileView } from '../../video-layout/actions';
@ -21,12 +23,12 @@ type Props = {
/**
* The user selected background color.
*/
_customBackgroundColor: string,
_customBackgroundColor: string,
/**
* The user selected background image url.
*/
_customBackgroundImageUrl: string,
_customBackgroundImageUrl: string,
/**
* Prop that indicates whether the chat is open.
@ -39,6 +41,21 @@ type Props = {
*/
_noAutoPlayVideo: boolean,
/**
* Whether or not the filmstrip is resizable.
*/
_resizableFilmstrip: boolean,
/**
* The width of the vertical filmstrip (user resized).
*/
_verticalFilmstripWidth: ?number,
/**
* Whether or not the filmstrip is visible.
*/
_visibleFilmstrip: boolean,
/**
* The Redux dispatch function.
*/
@ -54,6 +71,10 @@ type Props = {
class LargeVideo extends Component<Props> {
_tappedTimeout: ?TimeoutID;
_containerRef: Object;
_wrapperRef: Object;
/**
* Constructor of the component.
*
@ -62,8 +83,25 @@ class LargeVideo extends Component<Props> {
constructor(props) {
super(props);
this._containerRef = React.createRef();
this._wrapperRef = React.createRef();
this._clearTapTimeout = this._clearTapTimeout.bind(this);
this._onDoubleTap = this._onDoubleTap.bind(this);
this._updateLayout = this._updateLayout.bind(this);
}
/**
* Implements {@code Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps: Props) {
const { _visibleFilmstrip } = this.props;
if (prevProps._visibleFilmstrip !== _visibleFilmstrip) {
this._updateLayout();
}
}
/**
@ -84,6 +122,7 @@ class LargeVideo extends Component<Props> {
<div
className = { className }
id = 'largeVideoContainer'
ref = { this._containerRef }
style = { style }>
<SharedVideo />
<div id = 'etherpad' />
@ -112,6 +151,7 @@ class LargeVideo extends Component<Props> {
<div
id = 'largeVideoWrapper'
onTouchEnd = { this._onDoubleTap }
ref = { this._wrapperRef }
role = 'figure' >
<video
autoPlay = { !_noAutoPlayVideo }
@ -126,6 +166,32 @@ class LargeVideo extends Component<Props> {
);
}
_updateLayout: () => void;
/**
* Refreshes the video layout to determine the dimensions of the stage view.
* If the filmstrip is toggled it adds CSS transition classes and removes them
* when the transition is done.
*
* @returns {void}
*/
_updateLayout() {
const { _verticalFilmstripWidth, _resizableFilmstrip } = this.props;
if (_resizableFilmstrip && _verticalFilmstripWidth >= FILMSTRIP_BREAKPOINT) {
this._containerRef.current.classList.add('transition');
this._wrapperRef.current.classList.add('transition');
VideoLayout.refreshLayout();
setTimeout(() => {
this._containerRef.current && this._containerRef.current.classList.remove('transition');
this._wrapperRef.current && this._wrapperRef.current.classList.remove('transition');
}, 1000);
} else {
VideoLayout.refreshLayout();
}
}
_clearTapTimeout: () => void;
/**
@ -147,7 +213,12 @@ class LargeVideo extends Component<Props> {
*/
_getCustomSyles() {
const styles = {};
const { _customBackgroundColor, _customBackgroundImageUrl } = this.props;
const {
_customBackgroundColor,
_customBackgroundImageUrl,
_verticalFilmstripWidth,
_visibleFilmstrip
} = this.props;
styles.backgroundColor = _customBackgroundColor || interfaceConfig.DEFAULT_BACKGROUND;
@ -162,6 +233,10 @@ class LargeVideo extends Component<Props> {
styles.backgroundSize = 'cover';
}
if (_visibleFilmstrip && _verticalFilmstripWidth >= FILMSTRIP_BREAKPOINT) {
styles.width = `calc(100% - ${_verticalFilmstripWidth || 0}px)`;
}
return styles;
}
@ -199,13 +274,17 @@ function _mapStateToProps(state) {
const testingConfig = state['features/base/config'].testing;
const { backgroundColor, backgroundImageUrl } = state['features/dynamic-branding'];
const { isOpen: isChatOpen } = state['features/chat'];
const { width: verticalFilmstripWidth, visible } = state['features/filmstrip'];
return {
_backgroundAlpha: state['features/base/config'].backgroundAlpha,
_customBackgroundColor: backgroundColor,
_customBackgroundImageUrl: backgroundImageUrl,
_isChatOpen: isChatOpen,
_noAutoPlayVideo: testingConfig?.noAutoPlayVideo
_noAutoPlayVideo: testingConfig?.noAutoPlayVideo,
_resizableFilmstrip: isFilmstripResizable(state),
_verticalFilmstripWidth: verticalFilmstripWidth.current,
_visibleFilmstrip: visible
};
}

@ -59,9 +59,10 @@ export function getCurrentLayout(state: Object) {
* returned will be between 1 and 7, inclusive.
*
* @param {Object} state - The redux store state.
* @param {number} width - Custom width to use for calculation.
* @returns {number}
*/
export function getMaxColumnCount(state: Object) {
export function getMaxColumnCount(state: Object, width: ?number) {
const configuredMax = (typeof interfaceConfig === 'undefined'
? DEFAULT_MAX_COLUMNS
: interfaceConfig.TILE_VIEW_MAX_COLUMNS) || DEFAULT_MAX_COLUMNS;
@ -69,20 +70,21 @@ export function getMaxColumnCount(state: Object) {
if (!disableResponsiveTiles) {
const { clientWidth } = state['features/base/responsive-ui'];
const widthToUse = width || clientWidth;
const participantCount = getParticipantCount(state);
// If there are just two participants in a conference, enforce single-column view for mobile size.
if (participantCount === 2 && clientWidth < ASPECT_RATIO_BREAKPOINT) {
if (participantCount === 2 && widthToUse < ASPECT_RATIO_BREAKPOINT) {
return Math.min(1, Math.max(configuredMax, 1));
}
// Enforce single column view at very small screen widths.
if (clientWidth < SINGLE_COLUMN_BREAKPOINT) {
if (widthToUse < SINGLE_COLUMN_BREAKPOINT) {
return Math.min(1, Math.max(configuredMax, 1));
}
// Enforce two column view below breakpoint.
if (clientWidth < TWO_COLUMN_BREAKPOINT) {
if (widthToUse < TWO_COLUMN_BREAKPOINT) {
return Math.min(2, Math.max(configuredMax, 1));
}
}
@ -96,11 +98,12 @@ export function getMaxColumnCount(state: Object) {
* which rows will be added but no more columns.
*
* @param {Object} state - The redux store state.
* @param {number} width - Custom width to use for calculation.
* @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) {
const maxColumns = getMaxColumnCount(state);
export function getTileViewGridDimensions(state: Object, width: ?number) {
const maxColumns = getMaxColumnCount(state, width);
// When in tile view mode, we must discount ourselves (the local participant) because our
// tile is not visible.

@ -10,7 +10,6 @@ import {
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import { TRACK_ADDED, TRACK_REMOVED, TRACK_STOPPED } from '../base/tracks';
import { SET_FILMSTRIP_VISIBLE } from '../filmstrip';
import { PARTICIPANTS_PANE_CLOSE, PARTICIPANTS_PANE_OPEN } from '../participants-pane/actionTypes.js';
import './middleware.any';
@ -54,7 +53,6 @@ MiddlewareRegistry.register(store => next => action => {
case PARTICIPANTS_PANE_CLOSE:
case PARTICIPANTS_PANE_OPEN:
case SET_FILMSTRIP_VISIBLE:
VideoLayout.resizeVideoArea();
break;

Loading…
Cancel
Save