From 2dda749b1fbdc57ca8c4f89db796826058b143aa Mon Sep 17 00:00:00 2001 From: Robert Pintilii Date: Thu, 24 Feb 2022 14:20:37 +0200 Subject: [PATCH] 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 --- config.js | 7 + css/_subject.scss | 2 +- css/_videolayout_default.scss | 4 + css/filmstrip/_vertical_filmstrip.scss | 26 +- css/premeeting/_premeeting-screens.scss | 2 +- modules/UI/videolayout/LargeVideoManager.js | 7 + react/features/base/config/configWhitelist.js | 1 + react/features/filmstrip/actionTypes.js | 18 + react/features/filmstrip/actions.web.js | 96 ++++- .../filmstrip/components/web/Filmstrip.js | 331 ++++++++++++------ .../filmstrip/components/web/Thumbnail.js | 27 +- .../components/web/ThumbnailWrapper.js | 9 +- .../filmstrip/components/web/styles.js | 150 ++++++++ react/features/filmstrip/constants.js | 44 ++- react/features/filmstrip/functions.web.js | 84 ++++- react/features/filmstrip/middleware.web.js | 23 ++ react/features/filmstrip/reducer.js | 43 ++- react/features/filmstrip/subscriber.web.js | 14 + .../large-video/components/LargeVideo.web.js | 87 ++++- react/features/video-layout/functions.js | 15 +- react/features/video-layout/middleware.web.js | 2 - 21 files changed, 827 insertions(+), 165 deletions(-) create mode 100644 react/features/filmstrip/components/web/styles.js diff --git a/config.js b/config.js index ec78e9879..ed8f4a99c 100644 --- a/config.js +++ b/config.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, diff --git a/css/_subject.scss b/css/_subject.scss index 2504d8edb..34685c395 100644 --- a/css/_subject.scss +++ b/css/_subject.scss @@ -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; diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index 2fd87e309..70535316f 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -78,6 +78,10 @@ #largeVideoContainer { overflow: hidden; text-align: center; + + &.transition { + transition: width 1s, height 1s, top 1s; + } } #largeVideoContainer { diff --git a/css/filmstrip/_vertical_filmstrip.scss b/css/filmstrip/_vertical_filmstrip.scss index a1d674305..e98323b32 100644 --- a/css/filmstrip/_vertical_filmstrip.scss +++ b/css/filmstrip/_vertical_filmstrip.scss @@ -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; diff --git a/css/premeeting/_premeeting-screens.scss b/css/premeeting/_premeeting-screens.scss index 56675500a..9bc57aca4 100644 --- a/css/premeeting/_premeeting-screens.scss +++ b/css/premeeting/_premeeting-screens.scss @@ -7,7 +7,7 @@ position: absolute; right: 0; top: 0; - z-index: $toolbarZ + 1; + z-index: $toolbarZ + 2; .action-btn { border-radius: 6px; diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index cafc0b473..21b636352 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -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; } diff --git a/react/features/base/config/configWhitelist.js b/react/features/base/config/configWhitelist.js index 58c854114..336e09763 100644 --- a/react/features/base/config/configWhitelist.js +++ b/react/features/base/config/configWhitelist.js @@ -154,6 +154,7 @@ export default [ 'failICE', 'feedbackPercentage', 'fileRecordingsEnabled', + 'filmstrip', 'firefox_fake_device', 'forceJVB121Ratio', 'forceTurnRelay', diff --git a/react/features/filmstrip/actionTypes.js b/react/features/filmstrip/actionTypes.js index 3d0906f1b..194d66a3a 100644 --- a/react/features/filmstrip/actionTypes.js +++ b/react/features/filmstrip/actionTypes.js @@ -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'; diff --git a/react/features/filmstrip/actions.web.js b/react/features/filmstrip/actions.web.js index 079cc6c52..4480ca53a 100644 --- a/react/features/filmstrip/actions.web.js +++ b/react/features/filmstrip/actions.web.js @@ -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, 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 + }; +} diff --git a/react/features/filmstrip/components/web/Filmstrip.js b/react/features/filmstrip/components/web/Filmstrip.js index 4ff7256f5..b141e44e6 100644 --- a/react/features/filmstrip/components/web/Filmstrip.js +++ b/react/features/filmstrip/components/web/Filmstrip.js @@ -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' - }, +type State = { - toggleFilmstripButton: { - fontSize: '14px', - lineHeight: 1.2, - textAlign: 'center', - background: 'transparent', - height: 'auto', - width: '100%', - padding: 0, - margin: 0, - border: 'none', + /** + * Whether or not the mouse is pressed. + */ + isMouseDown: boolean, - '-webkit-appearance': 'none', + /** + * Initial mouse position on drag handle mouse down. + */ + mousePosition: ?number, - '& 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 - } - }, - - '.horizontal-filmstrip &.hidden': { - bottom: '-50px', - - '&:hover': { - backgroundColor: 'transparent' - } - }, - - '&.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 { +class Filmstrip extends PureComponent { + + _throttledResize: Function; /** * Initializes a new {@code Filmstrip} instance. @@ -243,6 +215,12 @@ class Filmstrip extends PureComponent { 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 { 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 { this._onShortcutToggleFilmstrip, 'keyboardShortcuts.toggleFilmstrip' ); + document.addEventListener('mouseup', this._onDragMouseUp); + document.addEventListener('mousemove', this._throttledResize); } /** @@ -275,6 +266,8 @@ class Filmstrip extends PureComponent { */ componentWillUnmount() { APP.keyboardshortcut.unregisterShortcut('F'); + document.removeEventListener('mouseup', this._onDragMouseUp); + document.removeEventListener('mousemove', this._throttledResize); } /** @@ -285,17 +278,32 @@ class Filmstrip extends PureComponent { */ 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 { toolbar = this._renderToggleButton(); } + const filmstrip = (<> +
+ {!_disableSelfView && !_verticalViewGrid && ( +
+
+ { + !tileViewActive && + } +
+
+ )} + { + this._renderRemoteParticipants() + } +
+ ); + return (
= FILMSTRIP_BREAKPOINT + && classes.filmstripBackground) } style = { filmstripStyle }> { toolbar } -
- {!_disableSelfView && ( + {_resizableFilmstrip + ?
-
- { - !tileViewActive && - } -
+ className = { clsx('dragHandleContainer', + classes.dragHandleContainer, + isMouseDown && 'visible') + } + onMouseDown = { this._onDragHandleMouseDown }> +
- )} - { - this._renderRemoteParticipants() - } -
+ {filmstrip} +
+ : filmstrip + }
); } + _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 { _remoteParticipantsLength, _rows, _thumbnailHeight, - _thumbnailWidth + _thumbnailWidth, + _verticalViewGrid } = this.props; if (!_thumbnailWidth || isNaN(_thumbnailWidth) || !_thumbnailHeight @@ -489,7 +574,7 @@ class Filmstrip extends PureComponent { return null; } - if (_currentLayout === LAYOUTS.TILE_VIEW) { + if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid) { return ( { 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 }; } diff --git a/react/features/filmstrip/components/web/Thumbnail.js b/react/features/filmstrip/components/web/Thumbnail.js index c4535e063..50f98668b 100644 --- a/react/features/filmstrip/components/web/Thumbnail.js +++ b/react/features/filmstrip/components/web/Thumbnail.js @@ -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 { 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; diff --git a/react/features/filmstrip/components/web/ThumbnailWrapper.js b/react/features/filmstrip/components/web/ThumbnailWrapper.js index 2d2d6aad9..8e49a2448 100644 --- a/react/features/filmstrip/components/web/ThumbnailWrapper.js +++ b/react/features/filmstrip/components/web/ThumbnailWrapper.js @@ -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; diff --git a/react/features/filmstrip/components/web/styles.js b/react/features/filmstrip/components/web/styles.js new file mode 100644 index 000000000..9623d8382 --- /dev/null +++ b/react/features/filmstrip/components/web/styles.js @@ -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' + } + }; +}; diff --git a/react/features/filmstrip/constants.js b/react/features/filmstrip/constants.js index 66c9ec65c..fd0efdbc7 100644 --- a/react/features/filmstrip/constants.js +++ b/react/features/filmstrip/constants.js @@ -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; diff --git a/react/features/filmstrip/functions.web.js b/react/features/filmstrip/functions.web.js index 4be1e7a87..a6cf4283f 100644 --- a/react/features/filmstrip/functions.web.js +++ b/react/features/filmstrip/functions.web.js @@ -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); +} diff --git a/react/features/filmstrip/middleware.web.js b/react/features/filmstrip/middleware.web.js index 1c5e4e734..e541aafad 100644 --- a/react/features/filmstrip/middleware.web.js +++ b/react/features/filmstrip/middleware.web.js @@ -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; diff --git a/react/features/filmstrip/reducer.js b/react/features/filmstrip/reducer.js index 40ccd2955..ff299c70a 100644 --- a/react/features/filmstrip/reducer.js +++ b/react/features/filmstrip/reducer.js @@ -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} */ - 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; diff --git a/react/features/filmstrip/subscriber.web.js b/react/features/filmstrip/subscriber.web.js index aed0afdd2..1a29d6302 100644 --- a/react/features/filmstrip/subscriber.web.js +++ b/react/features/filmstrip/subscriber.web.js @@ -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()); + }); diff --git a/react/features/large-video/components/LargeVideo.web.js b/react/features/large-video/components/LargeVideo.web.js index 8b4f003ce..c2fe88c93 100644 --- a/react/features/large-video/components/LargeVideo.web.js +++ b/react/features/large-video/components/LargeVideo.web.js @@ -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 { _tappedTimeout: ?TimeoutID; + _containerRef: Object; + + _wrapperRef: Object; + /** * Constructor of the component. * @@ -62,8 +83,25 @@ class LargeVideo extends Component { 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 {
@@ -112,6 +151,7 @@ class LargeVideo extends Component {