feat(Filmstrip): Pagination.
This commit is contained in:
parent
64ae9c7953
commit
16cfda3c7a
|
@ -33,18 +33,18 @@
|
|||
}
|
||||
|
||||
&__videos {
|
||||
@extend %align-right;
|
||||
position:relative;
|
||||
padding: 0;
|
||||
/* The filmstrip should not be covered by the left toolbar. */
|
||||
bottom: 0;
|
||||
width:auto;
|
||||
overflow: visible !important;
|
||||
|
||||
&#remoteVideos {
|
||||
border: $thumbnailsBorder solid transparent;
|
||||
transition: bottom 2s;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
@include minHWAutoFix()
|
||||
}
|
||||
|
||||
|
@ -60,41 +60,25 @@
|
|||
&.hidden {
|
||||
bottom: calc(-196px - #{$newToolbarSizeWithPadding});
|
||||
}
|
||||
|
||||
.remote-videos-container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.remote-videos-container {
|
||||
transition: opacity 1s;
|
||||
.remote-videos {
|
||||
& > div {
|
||||
transition: opacity 1s;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&.is-not-overflowing > div {
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide-videos {
|
||||
.remote-videos-container {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
#filmstripRemoteVideos {
|
||||
@include minHWAutoFix();
|
||||
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: auto;
|
||||
justify-content: flex-end;
|
||||
flex-direction: row;
|
||||
|
||||
#filmstripRemoteVideosContainer {
|
||||
flex-direction: row-reverse;
|
||||
/**
|
||||
* Add padding as a hack for Firefox not to show scrollbars when
|
||||
* unnecessary.
|
||||
*/
|
||||
padding: 1px 0;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
.remote-videos {
|
||||
& > div {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,25 +87,3 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Workarounds for Edge and Firefox not handling scrolling properly with
|
||||
* flex-direction: row-reverse.
|
||||
*/
|
||||
@mixin undoRowReverseVideos() {
|
||||
.horizontal-filmstrip {
|
||||
#remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Firefox detection hack **/
|
||||
@-moz-document url-prefix() {
|
||||
@include undoRowReverseVideos();
|
||||
}
|
||||
|
||||
/** Edge detection hack **/
|
||||
@supports (-ms-ime-align:auto) {
|
||||
@include undoRowReverseVideos();
|
||||
}
|
||||
|
|
|
@ -10,13 +10,11 @@
|
|||
box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px $videoThumbnailSelected;
|
||||
}
|
||||
|
||||
#filmstripRemoteVideos {
|
||||
.remote-videos {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filmstrip__videos .videocontainer {
|
||||
|
@ -34,6 +32,9 @@
|
|||
*/
|
||||
height: 100% !important;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
|
@ -50,6 +51,10 @@
|
|||
&.shift-right {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
|
||||
.remote-videos{
|
||||
width: calc(100vw - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,63 +67,49 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
#filmstripRemoteVideos {
|
||||
.remote-videos {
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
/**
|
||||
* Allow vertical scrolling of the thumbnails.
|
||||
*/
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of the thumbnails should be set with javascript, based on
|
||||
* desired column count and window width. The rows are created using flex
|
||||
* and allowing the thumbnails to wrap.
|
||||
*/
|
||||
#filmstripRemoteVideosContainer {
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
justify-content: center;
|
||||
|
||||
.videocontainer {
|
||||
border: 0;
|
||||
* The size of the thumbnails should be set with javascript, based on
|
||||
* desired column count and window width. The rows are created using flex
|
||||
* and allowing the thumbnails to wrap.
|
||||
*/
|
||||
& > div {
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 2px;
|
||||
}
|
||||
display: flex;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
|
||||
video {
|
||||
object-fit: contain;
|
||||
}
|
||||
.videocontainer {
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
|
||||
*/
|
||||
@media only screen and (max-width: 500px) {
|
||||
video {
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
|
||||
*/
|
||||
@media only screen and (max-width: 500px) {
|
||||
video {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.has-overflow#filmstripRemoteVideosContainer {
|
||||
align-content: baseline;
|
||||
}
|
||||
|
||||
.has-overflow .videocontainer {
|
||||
align-self: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.shift-right #filmstripRemoteVideosContainer {
|
||||
.shift-right .remote-videos > div {
|
||||
/**
|
||||
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants,
|
||||
* from which we subtract the chat size.
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
.vertical-filmstrip .filmstrip {
|
||||
&.hide-videos {
|
||||
.remote-videos-container {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
.remote-videos {
|
||||
& > div {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,10 +41,6 @@
|
|||
right: 0;
|
||||
z-index: $filmstripVideosZ;
|
||||
|
||||
&.reduce-height {
|
||||
height: calc(100% - #{$newToolbarSizeWithPadding});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide videos by making them slight to the right.
|
||||
*/
|
||||
|
@ -98,33 +96,10 @@
|
|||
* filmstrip from overlapping the left edge of the screen.
|
||||
*/
|
||||
#filmstripLocalVideo,
|
||||
#filmstripRemoteVideos {
|
||||
.remote-videos {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#filmstripRemoteVideos {
|
||||
@include minHWAutoFix();
|
||||
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column-reverse;
|
||||
height: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
#filmstripRemoteVideosContainer {
|
||||
@include minHWAutoFix();
|
||||
flex-direction: column-reverse;
|
||||
overflow: visible;
|
||||
width: calc(100% - 8px); // 8px for margin + border of the thumbnails
|
||||
|
||||
.videocontainer {
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#remoteVideos {
|
||||
@include minHWAutoFix();
|
||||
|
||||
|
@ -132,56 +107,21 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.remote-videos-container {
|
||||
&.reduce-height {
|
||||
height: calc(100% - calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight}));
|
||||
}
|
||||
|
||||
.remote-videos {
|
||||
display: flex;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
transition: height .3s ease-in;
|
||||
|
||||
.hide-scrollbar#filmstripRemoteVideos {
|
||||
margin-right: 7px; // Scrollbar size
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
& > div {
|
||||
position: absolute;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
|
||||
&.is-not-overflowing > div {
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Workarounds for Edge and Firefox not handling scrolling properly with
|
||||
* flex-direction: column-reverse. The remove videos in filmstrip should
|
||||
* start scrolling from the bottom of the filmstrip, but in those browsers the
|
||||
* scrolling won't happen. Per W3C spec, scrolling should happen from the
|
||||
* bottom. As such, use css hacks to get around the css issue, with the intent
|
||||
* being to remove the hacks as the spec is supported.
|
||||
*/
|
||||
@mixin undoColumnReverseVideos() {
|
||||
.vertical-filmstrip {
|
||||
#remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FF does not include the scroll width when calculating the size of the content. That's why we need to include
|
||||
* ourselves the width of the scroll so that the remote videos are aligned with the local one.
|
||||
*/
|
||||
@mixin filmstripSizeWithoutScroll {
|
||||
.vertical-filmstrip {
|
||||
#remoteVideos #filmstripRemoteVideos {
|
||||
#filmstripRemoteVideosContainer {
|
||||
width: calc(100% - 15px) // 8 px - margins + border of the thumbnails; 7px - for the scroll
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Firefox detection hack **/
|
||||
@-moz-document url-prefix() {
|
||||
@include undoColumnReverseVideos();
|
||||
@include filmstripSizeWithoutScroll();
|
||||
}
|
||||
|
||||
/** Edge detection hack **/
|
||||
@supports (-ms-ime-align:auto) {
|
||||
@include undoColumnReverseVideos();
|
||||
}
|
||||
|
|
|
@ -11390,6 +11390,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"memoize-one": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
|
||||
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
|
||||
},
|
||||
"memory-fs": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||
|
@ -15318,6 +15323,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react-window": {
|
||||
"version": "1.8.6",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
|
||||
"integrity": "sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
}
|
||||
},
|
||||
"react-youtube": {
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.13.1.tgz",
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
"react-textarea-autosize": "8.3.0",
|
||||
"react-transition-group": "2.4.0",
|
||||
"react-youtube": "7.13.1",
|
||||
"react-window": "1.8.6",
|
||||
"redux": "4.0.4",
|
||||
"redux-thunk": "2.2.0",
|
||||
"rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { createAudioPlayErrorEvent, createAudioPlaySuccessEvent, sendAnalytics } from '../../../../analytics';
|
||||
import { connect } from '../../../redux';
|
||||
import logger from '../../logger';
|
||||
|
||||
/**
|
||||
|
@ -10,6 +11,16 @@ import logger from '../../logger';
|
|||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Represents muted property of the underlying audio element.
|
||||
*/
|
||||
_muted: ?Boolean,
|
||||
|
||||
/**
|
||||
* Represents volume property of the underlying audio element.
|
||||
*/
|
||||
_volume: ?number,
|
||||
|
||||
/**
|
||||
* The value of the id attribute of the audio element.
|
||||
*/
|
||||
|
@ -28,26 +39,15 @@ type Props = {
|
|||
autoPlay: boolean,
|
||||
|
||||
/**
|
||||
* Represents muted property of the underlying audio element.
|
||||
* The ID of the participant associated with the audio element.
|
||||
*/
|
||||
muted: ?Boolean,
|
||||
|
||||
/**
|
||||
* Represents volume property of the underlying audio element.
|
||||
*/
|
||||
volume: ?number,
|
||||
|
||||
/**
|
||||
* A function that will be executed when the reference to the underlying audio element changes in order to report
|
||||
* the initial volume value.
|
||||
*/
|
||||
onInitialVolumeSet: Function
|
||||
participantId: string
|
||||
};
|
||||
|
||||
/**
|
||||
* The React/Web {@link Component} which is similar to and wraps around {@code HTMLAudioElement}.
|
||||
*/
|
||||
export default class AudioTrack extends Component<Props> {
|
||||
class AudioTrack extends Component<Props> {
|
||||
/**
|
||||
* Reference to the HTML audio element, stored until the file is ready.
|
||||
*/
|
||||
|
@ -94,14 +94,14 @@ export default class AudioTrack extends Component<Props> {
|
|||
this._attachTrack(this.props.audioTrack);
|
||||
|
||||
if (this._ref) {
|
||||
const { muted, volume } = this.props;
|
||||
const { _muted, _volume } = this.props;
|
||||
|
||||
if (typeof volume === 'number') {
|
||||
this._ref.volume = volume;
|
||||
if (typeof _volume === 'number') {
|
||||
this._ref.volume = _volume;
|
||||
}
|
||||
|
||||
if (typeof muted === 'boolean') {
|
||||
this._ref.muted = muted;
|
||||
if (typeof _muted === 'boolean') {
|
||||
this._ref.muted = _muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,14 +136,14 @@ export default class AudioTrack extends Component<Props> {
|
|||
|
||||
if (this._ref) {
|
||||
const currentVolume = this._ref.volume;
|
||||
const nextVolume = nextProps.volume;
|
||||
const nextVolume = nextProps._volume;
|
||||
|
||||
if (typeof nextVolume === 'number' && !isNaN(nextVolume) && currentVolume !== nextVolume) {
|
||||
this._ref.volume = nextVolume;
|
||||
}
|
||||
|
||||
const currentMuted = this._ref.muted;
|
||||
const nextMuted = nextProps.muted;
|
||||
const nextMuted = nextProps._muted;
|
||||
|
||||
if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
|
||||
this._ref.muted = nextMuted;
|
||||
|
@ -258,10 +258,24 @@ export default class AudioTrack extends Component<Props> {
|
|||
*/
|
||||
_setRef(audioElement: ?HTMLAudioElement) {
|
||||
this._ref = audioElement;
|
||||
const { onInitialVolumeSet } = this.props;
|
||||
|
||||
if (this._ref && onInitialVolumeSet) {
|
||||
onInitialVolumeSet(this._ref.volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code AudioTrack}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The props passed to the component.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { participantsVolume } = state['features/filmstrip'];
|
||||
|
||||
return {
|
||||
_muted: state['features/base/config'].startSilent,
|
||||
_volume: participantsVolume[ownProps.participantId]
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(AudioTrack);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default as Audio } from './Audio';
|
||||
export { default as AudioTrack } from './AudioTrack';
|
||||
export { default as Video } from './Video';
|
||||
export { default as VideoTrack } from './VideoTrack';
|
||||
|
|
|
@ -27,7 +27,7 @@ export const SET_FILMSTRIP_VISIBLE = 'SET_FILMSTRIP_VISIBLE';
|
|||
* gridDimensions: {
|
||||
* columns: number,
|
||||
* height: number,
|
||||
* visibleRows: number,
|
||||
* minVisibleRows: number,
|
||||
* width: number
|
||||
* },
|
||||
* thumbnailSize: {
|
||||
|
@ -49,3 +49,24 @@ export const SET_TILE_VIEW_DIMENSIONS = 'SET_TILE_VIEW_DIMENSIONS';
|
|||
* }
|
||||
*/
|
||||
export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the dimensions of the thumbnails in vertical view.
|
||||
*
|
||||
* {
|
||||
* type: SET_VERTICAL_VIEW_DIMENSIONS,
|
||||
* dimensions: Object
|
||||
* }
|
||||
*/
|
||||
export const SET_VERTICAL_VIEW_DIMENSIONS = 'SET_VERTICAL_VIEW_DIMENSIONS';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the volume for a thumnail's audio.
|
||||
*
|
||||
* {
|
||||
* type: SET_VOLUME,
|
||||
* participantId: string,
|
||||
* volume: number
|
||||
* }
|
||||
*/
|
||||
export const SET_VOLUME = 'SET_VOLUME';
|
||||
|
|
|
@ -1,64 +1,120 @@
|
|||
// @flow
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { pinParticipant } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
|
||||
import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
|
||||
import { calculateThumbnailSizeForHorizontalView, calculateThumbnailSizeForTileView } from './functions';
|
||||
|
||||
/**
|
||||
* The size of the side margins for the entire tile view area.
|
||||
*/
|
||||
const TILE_VIEW_SIDE_MARGINS = 20;
|
||||
import {
|
||||
SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
SET_TILE_VIEW_DIMENSIONS,
|
||||
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_MARGIN,
|
||||
VERTICAL_FILMSTRIP_VERTICAL_MARGIN
|
||||
} from './constants';
|
||||
import {
|
||||
calculateThumbnailSizeForHorizontalView,
|
||||
calculateThumbnailSizeForTileView,
|
||||
calculateThumbnailSizeForVerticalView
|
||||
} from './functions';
|
||||
|
||||
/**
|
||||
* Sets the dimensions of the tile view grid.
|
||||
*
|
||||
* @param {Object} dimensions - Whether the filmstrip is visible.
|
||||
* @param {Object} windowSize - The size of the window.
|
||||
* @param {Object | Function} stateful - An object or function that can be
|
||||
* resolved to Redux state using the {@code toState} function.
|
||||
* @returns {{
|
||||
* type: SET_TILE_VIEW_DIMENSIONS,
|
||||
* dimensions: Object
|
||||
* }}
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setTileViewDimensions(dimensions: Object, windowSize: Object, stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const { clientWidth, clientHeight } = windowSize;
|
||||
const { disableResponsiveTiles } = state['features/base/config'];
|
||||
export function setTileViewDimensions(dimensions: Object) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const { disableResponsiveTiles } = state['features/base/config'];
|
||||
const {
|
||||
height,
|
||||
width
|
||||
} = calculateThumbnailSizeForTileView({
|
||||
...dimensions,
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableResponsiveTiles
|
||||
});
|
||||
const { columns, rows } = dimensions;
|
||||
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
|
||||
const hasScroll = clientHeight < thumbnailsTotalHeight;
|
||||
const filmstripWidth = (columns * (TILE_HORIZONTAL_MARGIN + width)) + (hasScroll ? SCROLL_SIZE : 0);
|
||||
const filmstripHeight = Math.min(clientHeight, thumbnailsTotalHeight);
|
||||
|
||||
const thumbnailSize = calculateThumbnailSizeForTileView({
|
||||
...dimensions,
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableResponsiveTiles
|
||||
});
|
||||
const filmstripWidth = dimensions.columns * (TILE_VIEW_SIDE_MARGINS + thumbnailSize.width);
|
||||
dispatch({
|
||||
type: SET_TILE_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
gridDimensions: dimensions,
|
||||
thumbnailSize: {
|
||||
height,
|
||||
width
|
||||
},
|
||||
filmstripHeight,
|
||||
filmstripWidth
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: SET_TILE_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
gridDimensions: dimensions,
|
||||
thumbnailSize,
|
||||
filmstripWidth
|
||||
}
|
||||
/**
|
||||
* Sets the dimensions of the thumbnails in vertical view.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setVerticalViewDimensions() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
|
||||
const thumbnails = calculateThumbnailSizeForVerticalView(clientWidth);
|
||||
|
||||
dispatch({
|
||||
type: SET_VERTICAL_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
...thumbnails,
|
||||
remoteVideosContainer: {
|
||||
width: thumbnails?.local?.width
|
||||
+ TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER + SCROLL_SIZE,
|
||||
height: clientHeight - thumbnails?.local?.height - VERTICAL_FILMSTRIP_VERTICAL_MARGIN
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dimensions of the thumbnails in horizontal view.
|
||||
*
|
||||
* @param {number} clientHeight - The height of the window.
|
||||
* @returns {{
|
||||
* type: SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
* dimensions: Object
|
||||
* }}
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setHorizontalViewDimensions(clientHeight: number = 0) {
|
||||
return {
|
||||
type: SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
dimensions: calculateThumbnailSizeForHorizontalView(clientHeight)
|
||||
export function setHorizontalViewDimensions() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
|
||||
const thumbnails = calculateThumbnailSizeForHorizontalView(clientHeight);
|
||||
|
||||
dispatch({
|
||||
type: SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
...thumbnails,
|
||||
remoteVideosContainer: {
|
||||
width: clientWidth - thumbnails?.local?.width - HORIZONTAL_FILMSTRIP_MARGIN,
|
||||
height: thumbnails?.local?.height
|
||||
+ TILE_VERTICAL_MARGIN + STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER + SCROLL_SIZE
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -78,4 +134,23 @@ export function clickOnVideo(n: number) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume for a thumnail's audio.
|
||||
*
|
||||
* @param {string} participantId - The participant ID asociated with the audio.
|
||||
* @param {string} volume - The volume level.
|
||||
* @returns {{
|
||||
* type: SET_VOLUME,
|
||||
* participantId: string,
|
||||
* volume: number
|
||||
* }}
|
||||
*/
|
||||
export function setVolume(participantId: string, volume: number) {
|
||||
return {
|
||||
type: SET_VOLUME,
|
||||
participantId,
|
||||
volume
|
||||
};
|
||||
}
|
||||
|
||||
export * from './actions.native';
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/* @flow */
|
||||
import React from 'react';
|
||||
|
||||
import { AudioTrack, MEDIA_TYPE } from '../../../base/media';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioTracksContainer}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* All media tracks stored in redux.
|
||||
*/
|
||||
_tracks: Array<Object>
|
||||
};
|
||||
|
||||
/**
|
||||
* A container for the remote tracks audio elements.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns {Array<ReactElement>}
|
||||
*/
|
||||
function AudioTracksContainer(props: Props) {
|
||||
const { _tracks } = props;
|
||||
const remoteAudioTracks = _tracks.filter(t => !t.local && t.mediaType === MEDIA_TYPE.AUDIO);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
remoteAudioTracks.map(t => {
|
||||
const { jitsiTrack, participantId } = t;
|
||||
const audioTrackId = jitsiTrack && jitsiTrack.getId();
|
||||
const id = `remoteAudio_${audioTrackId || ''}`;
|
||||
|
||||
return (
|
||||
<AudioTrack
|
||||
audioTrack = { t }
|
||||
id = { id }
|
||||
key = { id }
|
||||
participantId = { participantId } />);
|
||||
})
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code AudioTracksContainer}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
// NOTE: The disadvantage of this approach is that the component will re-render on any track change.
|
||||
// One way to solve the problem would be to pass only the participant ID to the AudioTrack component and
|
||||
// find the corresponding track inside the AudioTrack's mapStateToProps. But currently this will be very
|
||||
// inefficient because features/base/tracks is an array and in order to find a track by participant ID
|
||||
// we need to go trough the array. Introducing a map participantID -> track could be beneficial in this case.
|
||||
return {
|
||||
_tracks: state['features/base/tracks']
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(AudioTracksContainer);
|
|
@ -1,6 +1,7 @@
|
|||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { FixedSizeList, FixedSizeGrid } from 'react-window';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import {
|
||||
|
@ -11,15 +12,17 @@ import {
|
|||
import { getToolbarButtons } from '../../../base/config';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { showToolbox } from '../../../toolbox/actions.web';
|
||||
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||
import { setFilmstripVisible } from '../../actions';
|
||||
import { TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN, TOOLBAR_HEIGHT } from '../../constants';
|
||||
import { shouldRemoteVideosBeVisible } from '../../functions';
|
||||
|
||||
import AudioTracksContainer from './AudioTracksContainer';
|
||||
import Thumbnail from './Thumbnail';
|
||||
import ThumbnailWrapper from './ThumbnailWrapper';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
@ -50,14 +53,9 @@ type Props = {
|
|||
_filmstripWidth: number,
|
||||
|
||||
/**
|
||||
* Whether the filmstrip scrollbar should be hidden or not.
|
||||
* The height of the filmstrip.
|
||||
*/
|
||||
_hideScrollbar: boolean,
|
||||
|
||||
/**
|
||||
* Whether the filmstrip toolbar should be hidden or not.
|
||||
*/
|
||||
_hideToolbar: boolean,
|
||||
_filmstripHeight: number,
|
||||
|
||||
/**
|
||||
* Whether the filmstrip button is enabled.
|
||||
|
@ -67,13 +65,29 @@ type Props = {
|
|||
/**
|
||||
* The participants in the call.
|
||||
*/
|
||||
_participants: Array<Object>,
|
||||
_remoteParticipants: Array<Object>,
|
||||
|
||||
|
||||
/**
|
||||
* The length of the remote participants array.
|
||||
*/
|
||||
_remoteParticipantsLength: number,
|
||||
|
||||
/**
|
||||
* The number of rows in tile view.
|
||||
*/
|
||||
_rows: number,
|
||||
|
||||
/**
|
||||
* The height of the thumbnail.
|
||||
*/
|
||||
_thumbnailHeight: number,
|
||||
|
||||
/**
|
||||
* The width of the thumbnail.
|
||||
*/
|
||||
_thumbnailWidth: number,
|
||||
|
||||
/**
|
||||
* Additional CSS class names to add to the container of all the thumbnails.
|
||||
*/
|
||||
|
@ -106,7 +120,7 @@ type Props = {
|
|||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class Filmstrip extends Component <Props> {
|
||||
class Filmstrip extends PureComponent <Props> {
|
||||
|
||||
/**
|
||||
* Initializes a new {@code Filmstrip} instance.
|
||||
|
@ -121,6 +135,8 @@ class Filmstrip extends Component <Props> {
|
|||
this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
|
||||
this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
|
||||
this._onTabIn = this._onTabIn.bind(this);
|
||||
this._gridItemKey = this._gridItemKey.bind(this);
|
||||
this._listItemKey = this._listItemKey.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -154,11 +170,7 @@ class Filmstrip extends Component <Props> {
|
|||
*/
|
||||
render() {
|
||||
const filmstripStyle = { };
|
||||
const filmstripRemoteVideosContainerStyle = {};
|
||||
let remoteVideoContainerClassName = 'remote-videos-container';
|
||||
const { _currentLayout, _participants } = this.props;
|
||||
const remoteParticipants = _participants.filter(p => !p.local);
|
||||
const localParticipant = getLocalParticipant(_participants);
|
||||
const { _currentLayout } = this.props;
|
||||
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
|
||||
|
||||
switch (_currentLayout) {
|
||||
|
@ -167,28 +179,11 @@ class Filmstrip extends Component <Props> {
|
|||
// Also adding 7px for the scrollbar.
|
||||
filmstripStyle.maxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120) + 25;
|
||||
break;
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
// The size of the side margins for each tile as set in CSS.
|
||||
const { _columns, _rows, _filmstripWidth } = this.props;
|
||||
|
||||
if (_rows > _columns) {
|
||||
remoteVideoContainerClassName += ' has-overflow';
|
||||
}
|
||||
|
||||
filmstripRemoteVideosContainerStyle.width = _filmstripWidth;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let remoteVideosWrapperClassName = 'filmstrip__videos';
|
||||
|
||||
if (this.props._hideScrollbar) {
|
||||
remoteVideosWrapperClassName += ' hide-scrollbar';
|
||||
}
|
||||
|
||||
let toolbar = null;
|
||||
|
||||
if (!this.props._hideToolbar && this.props._isFilmstripButtonEnabled) {
|
||||
if (this.props._isFilmstripButtonEnabled) {
|
||||
toolbar = this._renderToggleButton();
|
||||
}
|
||||
|
||||
|
@ -206,41 +201,15 @@ class Filmstrip extends Component <Props> {
|
|||
<div id = 'filmstripLocalVideoThumbnail'>
|
||||
{
|
||||
!tileViewActive && <Thumbnail
|
||||
key = 'local'
|
||||
participantID = { localParticipant.id } />
|
||||
key = 'local' />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className = { remoteVideosWrapperClassName }
|
||||
id = 'filmstripRemoteVideos'>
|
||||
{/*
|
||||
* XXX This extra video container is needed for
|
||||
* scrolling thumbnails in Firefox; otherwise, the flex
|
||||
* thumbnails resize instead of causing overflow.
|
||||
*/}
|
||||
<div
|
||||
className = { remoteVideoContainerClassName }
|
||||
id = 'filmstripRemoteVideosContainer'
|
||||
style = { filmstripRemoteVideosContainerStyle }>
|
||||
{
|
||||
remoteParticipants.map(
|
||||
p => (
|
||||
<Thumbnail
|
||||
key = { `remote_${p.id}` }
|
||||
participantID = { p.id } />
|
||||
))
|
||||
}
|
||||
<div id = 'localVideoTileViewContainer'>
|
||||
{
|
||||
tileViewActive && <Thumbnail
|
||||
key = 'local'
|
||||
participantID = { localParticipant.id } />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this._renderRemoteParticipants()
|
||||
}
|
||||
</div>
|
||||
<AudioTracksContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -258,6 +227,135 @@ class Filmstrip extends Component <Props> {
|
|||
}
|
||||
}
|
||||
|
||||
_listItemKey: number => string;
|
||||
|
||||
/**
|
||||
* The key to be used for every ThumbnailWrapper element in stage view.
|
||||
*
|
||||
* @param {number} index - The index of the ThumbnailWrapper instance.
|
||||
* @returns {string} - The key.
|
||||
*/
|
||||
_listItemKey(index) {
|
||||
const { _remoteParticipants, _remoteParticipantsLength } = this.props;
|
||||
|
||||
if (typeof index !== 'number' || _remoteParticipantsLength <= index) {
|
||||
return `empty-${index}`;
|
||||
}
|
||||
|
||||
return _remoteParticipants[_remoteParticipantsLength - index - 1];
|
||||
}
|
||||
|
||||
_gridItemKey: Object => string;
|
||||
|
||||
/**
|
||||
* The key to be used for every ThumbnailWrapper element in tile views.
|
||||
*
|
||||
* @param {Object} data - An object with the indexes identifying the ThumbnailWrapper instance.
|
||||
* @returns {string} - The key.
|
||||
*/
|
||||
_gridItemKey({ columnIndex, rowIndex }) {
|
||||
const { _columns, _remoteParticipants, _remoteParticipantsLength } = this.props;
|
||||
const index = (rowIndex * _columns) + columnIndex;
|
||||
|
||||
if (index > _remoteParticipantsLength) {
|
||||
return `empty-${index}`;
|
||||
}
|
||||
|
||||
if (index === _remoteParticipantsLength) {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
return _remoteParticipants[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the thumbnails for remote participants.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderRemoteParticipants() {
|
||||
const {
|
||||
_columns,
|
||||
_currentLayout,
|
||||
_filmstripHeight,
|
||||
_filmstripWidth,
|
||||
_remoteParticipantsLength,
|
||||
_rows,
|
||||
_thumbnailHeight,
|
||||
_thumbnailWidth
|
||||
} = this.props;
|
||||
|
||||
if (!_thumbnailWidth || isNaN(_thumbnailWidth) || !_thumbnailHeight
|
||||
|| isNaN(_thumbnailHeight) || !_filmstripHeight || isNaN(_filmstripHeight) || !_filmstripWidth
|
||||
|| isNaN(_filmstripWidth)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
return (
|
||||
<FixedSizeGrid
|
||||
className = 'filmstrip__videos remote-videos'
|
||||
columnCount = { _columns }
|
||||
columnWidth = { _thumbnailWidth + TILE_HORIZONTAL_MARGIN }
|
||||
height = { _filmstripHeight }
|
||||
initialScrollLeft = { 0 }
|
||||
initialScrollTop = { 0 }
|
||||
itemKey = { this._gridItemKey }
|
||||
rowCount = { _rows }
|
||||
rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN }
|
||||
width = { _filmstripWidth }>
|
||||
{
|
||||
ThumbnailWrapper
|
||||
}
|
||||
</FixedSizeGrid>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const props = {
|
||||
itemCount: _remoteParticipantsLength,
|
||||
className: 'filmstrip__videos remote-videos',
|
||||
height: _filmstripHeight,
|
||||
itemKey: this._listItemKey,
|
||||
itemSize: 0,
|
||||
width: _filmstripWidth,
|
||||
style: {
|
||||
willChange: 'auto'
|
||||
}
|
||||
};
|
||||
|
||||
if (_currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
|
||||
const itemSize = _thumbnailWidth + TILE_HORIZONTAL_MARGIN;
|
||||
const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripWidth;
|
||||
|
||||
props.itemSize = itemSize;
|
||||
|
||||
// $FlowFixMe
|
||||
props.layout = 'horizontal';
|
||||
if (isNotOverflowing) {
|
||||
props.className += ' is-not-overflowing';
|
||||
}
|
||||
|
||||
} else if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||
const itemSize = _thumbnailHeight + TILE_VERTICAL_MARGIN;
|
||||
const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripHeight;
|
||||
|
||||
if (isNotOverflowing) {
|
||||
props.className += ' is-not-overflowing';
|
||||
}
|
||||
|
||||
props.itemSize = itemSize;
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList { ...props }>
|
||||
{
|
||||
ThumbnailWrapper
|
||||
}
|
||||
</FixedSizeList>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to change the visibility of the filmstrip.
|
||||
*
|
||||
|
@ -344,29 +442,60 @@ class Filmstrip extends Component <Props> {
|
|||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { iAmSipGateway } = state['features/base/config'];
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const { visible } = state['features/filmstrip'];
|
||||
const reduceHeight
|
||||
= state['features/toolbox'].visible && toolbarButtons.length;
|
||||
const { visible, remoteParticipants } = state['features/filmstrip'];
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
|
||||
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
|
||||
const { isOpen: shiftRight } = state['features/chat'];
|
||||
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
|
||||
reduceHeight ? 'reduce-height' : ''
|
||||
} ${shiftRight ? 'shift-right' : ''}`.trim();
|
||||
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
|
||||
const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
|
||||
const {
|
||||
gridDimensions = {},
|
||||
filmstripHeight,
|
||||
filmstripWidth,
|
||||
thumbnailSize: tileViewThumbnailSize
|
||||
} = state['features/filmstrip'].tileViewDimensions;
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
_thumbnailSize = tileViewThumbnailSize;
|
||||
remoteFilmstripHeight = filmstripHeight;
|
||||
remoteFilmstripWidth = filmstripWidth;
|
||||
break;
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
const { remote, remoteVideosContainer } = state['features/filmstrip'].verticalViewDimensions;
|
||||
|
||||
_thumbnailSize = remote;
|
||||
remoteFilmstripHeight = remoteVideosContainer?.height - (reduceHeight ? TOOLBAR_HEIGHT : 0);
|
||||
remoteFilmstripWidth = remoteVideosContainer?.width;
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
const { remote, remoteVideosContainer } = state['features/filmstrip'].horizontalViewDimensions;
|
||||
|
||||
_thumbnailSize = remote;
|
||||
remoteFilmstripHeight = remoteVideosContainer?.height;
|
||||
remoteFilmstripWidth = remoteVideosContainer?.width;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
_className: className,
|
||||
_columns: gridDimensions.columns,
|
||||
_currentLayout: getCurrentLayout(state),
|
||||
_filmstripWidth: filmstripWidth,
|
||||
_hideScrollbar: Boolean(iAmSipGateway),
|
||||
_hideToolbar: Boolean(iAmSipGateway),
|
||||
_currentLayout,
|
||||
_filmstripHeight: remoteFilmstripHeight,
|
||||
_filmstripWidth: remoteFilmstripWidth,
|
||||
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
|
||||
_participants: state['features/base/participants'],
|
||||
_remoteParticipantsLength: remoteParticipants.length,
|
||||
_remoteParticipants: remoteParticipants,
|
||||
_rows: gridDimensions.rows,
|
||||
_thumbnailWidth: _thumbnailSize?.width,
|
||||
_thumbnailHeight: _thumbnailSize?.height,
|
||||
_videosClassName: videosClassName,
|
||||
_visible: visible,
|
||||
_isToolboxVisible: isToolboxVisible(state)
|
||||
|
|
|
@ -7,7 +7,6 @@ import { AudioLevelIndicator } from '../../../audio-level-indicator';
|
|||
import { Avatar } from '../../../base/avatar';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
|
||||
import AudioTrack from '../../../base/media/components/web/AudioTrack';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
|
@ -28,6 +27,7 @@ import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from
|
|||
import { PresenceLabel } from '../../../presence-status';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
|
||||
import { setVolume } from '../../actions.web';
|
||||
import {
|
||||
DISPLAY_MODE_TO_CLASS_NAME,
|
||||
DISPLAY_MODE_TO_STRING,
|
||||
|
@ -65,12 +65,7 @@ export type State = {|
|
|||
/**
|
||||
* Indicates whether the thumbnail is hovered or not.
|
||||
*/
|
||||
isHovered: boolean,
|
||||
|
||||
/**
|
||||
* The current volume setting for the Thumbnail.
|
||||
*/
|
||||
volume: ?number
|
||||
isHovered: boolean
|
||||
|};
|
||||
|
||||
/**
|
||||
|
@ -179,9 +174,9 @@ export type Props = {|
|
|||
_participant: Object,
|
||||
|
||||
/**
|
||||
* The number of participants in the call.
|
||||
* True if there are more than 2 participants in the call.
|
||||
*/
|
||||
_participantCount: number,
|
||||
_participantCountMoreThan2: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether the "start silent" mode is enabled.
|
||||
|
@ -193,6 +188,11 @@ export type Props = {|
|
|||
*/
|
||||
_videoTrack: ?Object,
|
||||
|
||||
/**
|
||||
* The volume level for the thumbnail.
|
||||
*/
|
||||
_volume?: ?number,
|
||||
|
||||
/**
|
||||
* The width of the thumbnail.
|
||||
*/
|
||||
|
@ -203,10 +203,20 @@ export type Props = {|
|
|||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
|
||||
*/
|
||||
horizontalOffset: number,
|
||||
|
||||
/**
|
||||
* The ID of the participant related to the thumbnail.
|
||||
*/
|
||||
participantID: ?string
|
||||
participantID: ?string,
|
||||
|
||||
/**
|
||||
* Styles that will be set to the Thumbnail's main span element.
|
||||
*/
|
||||
style?: ?Object
|
||||
|};
|
||||
|
||||
/**
|
||||
|
@ -240,7 +250,6 @@ class Thumbnail extends Component<Props, State> {
|
|||
audioLevel: 0,
|
||||
canPlayEventReceived: false,
|
||||
isHovered: false,
|
||||
volume: undefined,
|
||||
displayMode: DISPLAY_VIDEO
|
||||
};
|
||||
|
||||
|
@ -253,7 +262,6 @@ class Thumbnail extends Component<Props, State> {
|
|||
this._onCanPlay = this._onCanPlay.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onVolumeChange = this._onVolumeChange.bind(this);
|
||||
this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
|
||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
this._onTestingEvent = this._onTestingEvent.bind(this);
|
||||
|
@ -457,7 +465,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
* @returns {Object} - The styles for the thumbnail.
|
||||
*/
|
||||
_getStyles(): Object {
|
||||
const { _height, _heightToWidthPercent, _currentLayout, _isHidden, _width } = this.props;
|
||||
const { _height, _isHidden, _width, style, horizontalOffset } = this.props;
|
||||
let styles: {
|
||||
thumbnail: Object,
|
||||
avatar: Object
|
||||
|
@ -466,39 +474,28 @@ class Thumbnail extends Component<Props, State> {
|
|||
avatar: {}
|
||||
};
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
const avatarSize = _height / 2;
|
||||
const avatarSize = _height / 2;
|
||||
let { left } = style || {};
|
||||
|
||||
styles = {
|
||||
thumbnail: {
|
||||
height: `${_height}px`,
|
||||
minHeight: `${_height}px`,
|
||||
minWidth: `${_width}px`,
|
||||
width: `${_width}px`
|
||||
},
|
||||
avatar: {
|
||||
height: `${avatarSize}px`,
|
||||
width: `${avatarSize}px`
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
styles = {
|
||||
thumbnail: {
|
||||
paddingTop: `${_heightToWidthPercent}%`
|
||||
},
|
||||
avatar: {
|
||||
height: '50%',
|
||||
width: `${_heightToWidthPercent / 2}%`
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
if (typeof left === 'number' && horizontalOffset) {
|
||||
left += horizontalOffset;
|
||||
}
|
||||
|
||||
styles = {
|
||||
thumbnail: {
|
||||
...style,
|
||||
left,
|
||||
height: `${_height}px`,
|
||||
minHeight: `${_height}px`,
|
||||
minWidth: `${_width}px`,
|
||||
width: `${_width}px`
|
||||
},
|
||||
avatar: {
|
||||
height: `${avatarSize}px`,
|
||||
width: `${avatarSize}px`
|
||||
}
|
||||
};
|
||||
|
||||
if (_isHidden) {
|
||||
styles.thumbnail.display = 'none';
|
||||
}
|
||||
|
@ -584,7 +581,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
_isDominantSpeakerDisabled,
|
||||
_indicatorIconSize: iconSize,
|
||||
_participant,
|
||||
_participantCount
|
||||
_participantCountMoreThan2
|
||||
} = this.props;
|
||||
const { isHovered } = this.state;
|
||||
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
|
||||
|
@ -621,7 +618,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
iconSize = { iconSize }
|
||||
participantId = { id }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
{ showDominantSpeaker && _participantCount > 2
|
||||
{ showDominantSpeaker && _participantCountMoreThan2
|
||||
&& <DominantSpeakerIndicator
|
||||
iconSize = { iconSize }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
|
@ -793,21 +790,19 @@ class Thumbnail extends Component<Props, State> {
|
|||
*/
|
||||
_renderRemoteParticipant() {
|
||||
const {
|
||||
_audioTrack,
|
||||
_isTestModeEnabled,
|
||||
_participant,
|
||||
_startSilent,
|
||||
_videoTrack
|
||||
_videoTrack,
|
||||
_volume = 1
|
||||
} = this.props;
|
||||
const { id } = _participant;
|
||||
const { audioLevel, canPlayEventReceived, volume } = this.state;
|
||||
const { audioLevel, canPlayEventReceived } = this.state;
|
||||
const styles = this._getStyles();
|
||||
const containerClassName = this._getContainerClassName();
|
||||
|
||||
// hide volume when in silent mode
|
||||
const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
|
||||
const jitsiAudioTrack = _audioTrack?.jitsiTrack;
|
||||
const audioTrackId = jitsiAudioTrack && jitsiAudioTrack.getId();
|
||||
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
|
||||
const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
|
||||
const videoEventListeners = {};
|
||||
|
@ -840,14 +835,6 @@ class Thumbnail extends Component<Props, State> {
|
|||
style = { videoElementStyle }
|
||||
videoTrack = { _videoTrack } />
|
||||
}
|
||||
{
|
||||
_audioTrack && <AudioTrack
|
||||
audioTrack = { _audioTrack }
|
||||
id = { `remoteAudio_${audioTrackId || ''}` }
|
||||
muted = { _startSilent }
|
||||
onInitialVolumeSet = { this._onInitialVolumeSet }
|
||||
volume = { volume } />
|
||||
}
|
||||
<div className = 'videocontainer__background' />
|
||||
<div className = 'videocontainer__toptoolbar'>
|
||||
{ this._renderTopIndicators() }
|
||||
|
@ -872,7 +859,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
</span>
|
||||
<span className = 'remotevideomenu'>
|
||||
<RemoteVideoMenuTriggerButton
|
||||
initialVolumeValue = { volume }
|
||||
initialVolumeValue = { _volume }
|
||||
onVolumeChange = { onVolumeChange }
|
||||
participantID = { id } />
|
||||
</span>
|
||||
|
@ -880,20 +867,6 @@ class Thumbnail extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
_onInitialVolumeSet: Object => void;
|
||||
|
||||
/**
|
||||
* A handler for the initial volume value of the audio element.
|
||||
*
|
||||
* @param {number} volume - Properties of the audio element.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onInitialVolumeSet(volume) {
|
||||
if (this.state.volume !== volume) {
|
||||
this.setState({ volume });
|
||||
}
|
||||
}
|
||||
|
||||
_onVolumeChange: number => void;
|
||||
|
||||
/**
|
||||
|
@ -903,7 +876,10 @@ class Thumbnail extends Component<Props, State> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onVolumeChange(value) {
|
||||
this.setState({ volume: value });
|
||||
const { _participant, dispatch } = this.props;
|
||||
const { id } = _participant;
|
||||
|
||||
dispatch(setVolume(id, value));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -949,6 +925,7 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
const { id } = participant;
|
||||
const isLocal = participant?.local ?? true;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const { participantsVolume } = state['features/filmstrip'];
|
||||
const _videoTrack = isLocal
|
||||
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
|
||||
const _audioTrack = isLocal
|
||||
|
@ -967,14 +944,21 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
const {
|
||||
horizontalViewDimensions = {
|
||||
local: {},
|
||||
remote: {}
|
||||
},
|
||||
verticalViewDimensions = {
|
||||
local: {},
|
||||
remote: {}
|
||||
}
|
||||
} = state['features/filmstrip'];
|
||||
const { local, remote } = horizontalViewDimensions;
|
||||
const { local, remote }
|
||||
= _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
|
||||
? verticalViewDimensions : horizontalViewDimensions;
|
||||
const { width, height } = isLocal ? local : remote;
|
||||
|
||||
size = {
|
||||
|
@ -984,13 +968,6 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
size = {
|
||||
_heightToWidthPercent: isLocal
|
||||
? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
|
||||
: 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
|
||||
};
|
||||
break;
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
|
||||
|
||||
|
@ -1020,9 +997,10 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
_indicatorIconSize: NORMAL,
|
||||
_localFlipX: Boolean(localFlipX),
|
||||
_participant: participant,
|
||||
_participantCount: getParticipantCount(state),
|
||||
_participantCountMoreThan2: getParticipantCount(state) > 2,
|
||||
_startSilent: Boolean(startSilent),
|
||||
_videoTrack,
|
||||
_volume: isLocal ? undefined : participantsVolume[id],
|
||||
...size
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
/* @flow */
|
||||
import React, { Component } from 'react';
|
||||
import { shouldComponentUpdate } from 'react-window';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
|
||||
import Thumbnail from './Thumbnail';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ThumbnailWrapper}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The ID of the participant associated with the Thumbnail.
|
||||
*/
|
||||
_participantID: ?string,
|
||||
|
||||
/**
|
||||
* The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
|
||||
*/
|
||||
_horizontalOffset: number,
|
||||
|
||||
/**
|
||||
* The index of the column in tile view.
|
||||
*/
|
||||
columnIndex?: number,
|
||||
|
||||
/**
|
||||
* The index of the ThumbnailWrapper in stage view.
|
||||
*/
|
||||
index?: number,
|
||||
|
||||
/**
|
||||
* The index of the row in tile view.
|
||||
*/
|
||||
rowIndex?: number,
|
||||
|
||||
/**
|
||||
* The styles comming from react-window.
|
||||
*/
|
||||
style: Object
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper Component for the Thumbnail that translates the react-window specific props
|
||||
* to the Thumbnail Component's props.
|
||||
*/
|
||||
class ThumbnailWrapper extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Creates new ThumbnailWrapper instance.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.shouldComponentUpdate = shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate: Props => boolean;
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _participantID, style, _horizontalOffset = 0 } = this.props;
|
||||
|
||||
if (typeof _participantID !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_participantID === 'local') {
|
||||
return (
|
||||
<Thumbnail
|
||||
horizontalOffset = { _horizontalOffset }
|
||||
key = 'local'
|
||||
style = { style } />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Thumbnail
|
||||
horizontalOffset = { _horizontalOffset }
|
||||
key = { `remote_${_participantID}` }
|
||||
participantID = { _participantID }
|
||||
style = { style } />);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code ThumbnailWrapper}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The props passed to the component.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
const { remoteParticipants } = state['features/filmstrip'];
|
||||
const remoteParticipantsLength = remoteParticipants.length;
|
||||
|
||||
if (_currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
const { columnIndex, rowIndex } = ownProps;
|
||||
const { gridDimensions = {}, thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
|
||||
const { columns, rows } = gridDimensions;
|
||||
const index = (rowIndex * columns) + columnIndex;
|
||||
let horizontalOffset;
|
||||
|
||||
if (rowIndex === rows - 1) { // center the last row
|
||||
const { width: thumbnailWidth } = thumbnailSize;
|
||||
const participantsInTheLastRow = (remoteParticipantsLength + 1) % columns;
|
||||
|
||||
if (participantsInTheLastRow > 0) {
|
||||
horizontalOffset = Math.floor((columns - participantsInTheLastRow) * (thumbnailWidth + 4) / 2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (index > remoteParticipantsLength) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index === remoteParticipantsLength) {
|
||||
return {
|
||||
_participantID: 'local',
|
||||
_horizontalOffset: horizontalOffset
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
_participantID: remoteParticipants[index],
|
||||
_horizontalOffset: horizontalOffset
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const { index } = ownProps;
|
||||
|
||||
if (typeof index !== 'number' || remoteParticipantsLength <= index) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
_participantID: remoteParticipants[index]
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(ThumbnailWrapper);
|
|
@ -143,3 +143,68 @@ export const DISPLAY_MODE_TO_STRING = [
|
|||
'video-with-name',
|
||||
'avatar-with-name'
|
||||
];
|
||||
|
||||
/**
|
||||
* The vertical margin of a tile.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const TILE_VERTICAL_MARGIN = 4;
|
||||
|
||||
/**
|
||||
* The horizontal margin of a tile.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const TILE_HORIZONTAL_MARGIN = 4;
|
||||
|
||||
/**
|
||||
* The height of the whole toolbar.
|
||||
*/
|
||||
export const TOOLBAR_HEIGHT = 72;
|
||||
|
||||
/**
|
||||
* The size of the horizontal border of a thumbnail.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER = 4;
|
||||
|
||||
/**
|
||||
* The size of the vertical border of a thumbnail.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER = 4;
|
||||
|
||||
/**
|
||||
* The size of the scroll.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const SCROLL_SIZE = 7;
|
||||
|
||||
/**
|
||||
* The total vertical space between the thumbnails container and the edges of the window.
|
||||
*
|
||||
* NOTE: This will include margins, paddings and the space for the 'hide filmstrip' icon.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const VERTICAL_FILMSTRIP_VERTICAL_MARGIN = 60;
|
||||
|
||||
/**
|
||||
* The min horizontal space between the thumbnails container and the edges of the window.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN = 10;
|
||||
|
||||
/**
|
||||
* The total horizontal space between the thumbnails container and the edges of the window.
|
||||
*
|
||||
* NOTE: This will include margins, paddings and the space for the 'hide filmstrip' icon.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
|
||||
|
|
|
@ -23,16 +23,17 @@ import {
|
|||
DISPLAY_BLACKNESS_WITH_NAME,
|
||||
DISPLAY_VIDEO,
|
||||
DISPLAY_VIDEO_WITH_NAME,
|
||||
SCROLL_SIZE,
|
||||
SQUARE_TILE_ASPECT_RATIO,
|
||||
TILE_ASPECT_RATIO
|
||||
STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
|
||||
TILE_ASPECT_RATIO,
|
||||
TILE_HORIZONTAL_MARGIN,
|
||||
TILE_VERTICAL_MARGIN,
|
||||
VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
|
||||
} from './constants';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
// Minimum space to keep between the sides of the tiles and the sides
|
||||
// of the window.
|
||||
const TILE_VIEW_SIDE_MARGINS = 20;
|
||||
|
||||
/**
|
||||
* Returns true if the filmstrip on mobile is visible, false otherwise.
|
||||
*
|
||||
|
@ -139,15 +140,42 @@ export function calculateThumbnailSizeForHorizontalView(clientHeight: number = 0
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the size for thumbnails when in vertical view layout.
|
||||
*
|
||||
* @param {number} clientWidth - The height of the app window.
|
||||
* @returns {{local: {height, width}, remote: {height, width}}}
|
||||
*/
|
||||
export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0) {
|
||||
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);
|
||||
|
||||
return {
|
||||
local: {
|
||||
height: Math.floor(availableWidth / interfaceConfig.LOCAL_THUMBNAIL_RATIO),
|
||||
width: availableWidth
|
||||
},
|
||||
remote: {
|
||||
height: Math.floor(availableWidth / interfaceConfig.REMOTE_THUMBNAIL_RATIO),
|
||||
width: availableWidth
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the size for thumbnails when in tile view layout.
|
||||
*
|
||||
* @param {Object} dimensions - The desired dimensions of the tile view grid.
|
||||
* @returns {{height, width}}
|
||||
* @returns {{hasScroll, height, width}}
|
||||
*/
|
||||
export function calculateThumbnailSizeForTileView({
|
||||
columns,
|
||||
visibleRows,
|
||||
minVisibleRows,
|
||||
rows,
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableResponsiveTiles
|
||||
|
@ -158,12 +186,29 @@ export function calculateThumbnailSizeForTileView({
|
|||
aspectRatio = SQUARE_TILE_ASPECT_RATIO;
|
||||
}
|
||||
|
||||
const viewWidth = clientWidth - TILE_VIEW_SIDE_MARGINS;
|
||||
const viewHeight = clientHeight - TILE_VIEW_SIDE_MARGINS;
|
||||
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN);
|
||||
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN);
|
||||
const initialWidth = viewWidth / columns;
|
||||
const initialHeight = viewHeight / minVisibleRows;
|
||||
const aspectRatioHeight = initialWidth / aspectRatio;
|
||||
const height = Math.floor(Math.min(aspectRatioHeight, viewHeight / visibleRows));
|
||||
const width = Math.floor(aspectRatio * height);
|
||||
const noScrollHeight = (clientHeight / rows) - TILE_VERTICAL_MARGIN;
|
||||
const scrollInitialWidth = (viewWidth - SCROLL_SIZE) / columns;
|
||||
let height = Math.floor(Math.min(aspectRatioHeight, initialHeight));
|
||||
let width = Math.floor(aspectRatio * height);
|
||||
|
||||
if (height > noScrollHeight && width > scrollInitialWidth) { // we will have scroll and we need more space for it.
|
||||
const scrollAspectRatioHeight = scrollInitialWidth / aspectRatio;
|
||||
|
||||
// Recalculating width/height to fit the available space when a scroll is displayed.
|
||||
// NOTE: Math.min(scrollAspectRatioHeight, initialHeight) would be enough to recalculate but since the new
|
||||
// height value can theoretically be dramatically smaller and the scroll may not be neccessary anymore we need
|
||||
// to compare it with noScrollHeight( the optimal height to fit all thumbnails without scroll) and get the
|
||||
// bigger one. This way we ensure that we always strech the thumbnails as close as we can to the edges of the
|
||||
// window.
|
||||
height = Math.floor(Math.max(Math.min(scrollAspectRatioHeight, initialHeight), noScrollHeight));
|
||||
width = Math.floor(aspectRatio * height);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
height,
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
LAYOUTS
|
||||
} from '../video-layout';
|
||||
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
|
||||
|
||||
import './subscriber.web';
|
||||
|
||||
|
@ -27,22 +27,16 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
switch (layout) {
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
const { gridDimensions } = state['features/filmstrip'].tileViewDimensions;
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
store.dispatch(
|
||||
setTileViewDimensions(
|
||||
gridDimensions,
|
||||
{
|
||||
clientHeight,
|
||||
clientWidth
|
||||
},
|
||||
store
|
||||
)
|
||||
);
|
||||
store.dispatch(setTileViewDimensions(gridDimensions));
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
|
||||
store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
|
||||
store.dispatch(setHorizontalViewDimensions());
|
||||
break;
|
||||
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
store.dispatch(setVerticalViewDimensions());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
// @flow
|
||||
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
SET_FILMSTRIP_ENABLED,
|
||||
SET_FILMSTRIP_VISIBLE,
|
||||
SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
SET_TILE_VIEW_DIMENSIONS
|
||||
SET_TILE_VIEW_DIMENSIONS,
|
||||
SET_VERTICAL_VIEW_DIMENSIONS,
|
||||
SET_VOLUME
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
|
@ -26,6 +29,21 @@ const DEFAULT_STATE = {
|
|||
*/
|
||||
horizontalViewDimensions: {},
|
||||
|
||||
/**
|
||||
* The custom audio volume levels per perticipant.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
participantsVolume: {},
|
||||
|
||||
/**
|
||||
* The ordered IDs of the remote participants displayed in the filmstrip.
|
||||
*
|
||||
* NOTE: Currently the order will match the one from the base/participants array. But this is good initial step for
|
||||
* reordering the remote participants.
|
||||
*/
|
||||
remoteParticipants: [],
|
||||
|
||||
/**
|
||||
* The tile view dimensions.
|
||||
*
|
||||
|
@ -34,6 +52,14 @@ const DEFAULT_STATE = {
|
|||
*/
|
||||
tileViewDimensions: {},
|
||||
|
||||
/**
|
||||
* The vertical view dimensions.
|
||||
*
|
||||
* @public
|
||||
* @type {Object}
|
||||
*/
|
||||
verticalViewDimensions: {},
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the {@link Filmstrip} is visible.
|
||||
*
|
||||
|
@ -69,6 +95,44 @@ ReducerRegistry.register(
|
|||
...state,
|
||||
tileViewDimensions: action.dimensions
|
||||
};
|
||||
case SET_VERTICAL_VIEW_DIMENSIONS:
|
||||
return {
|
||||
...state,
|
||||
verticalViewDimensions: action.dimensions
|
||||
};
|
||||
case SET_VOLUME:
|
||||
return {
|
||||
...state,
|
||||
participantsVolume: {
|
||||
...state.participantsVolume,
|
||||
|
||||
// NOTE: This would fit better in the features/base/participants. But currently we store
|
||||
// the participants as an array which will make it expensive to search for the volume for
|
||||
// every participant separately.
|
||||
[action.participantId]: action.volume
|
||||
}
|
||||
};
|
||||
case PARTICIPANT_JOINED: {
|
||||
const { id, local } = action.participant;
|
||||
|
||||
if (!local) {
|
||||
state.remoteParticipants = [ ...state.remoteParticipants, id ];
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
const { id, local } = action.participant;
|
||||
|
||||
if (local) {
|
||||
return state;
|
||||
}
|
||||
|
||||
state.remoteParticipants = state.remoteParticipants.filter(participantId => participantId !== id);
|
||||
delete state.participantsVolume[id];
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
|
|
|
@ -7,7 +7,7 @@ import { getParticipantsPaneOpen } from '../participants-pane/functions';
|
|||
import { setOverflowDrawer } from '../toolbox/actions.web';
|
||||
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
|
||||
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
DISPLAY_DRAWER_THRESHOLD,
|
||||
|
@ -28,18 +28,7 @@ StateListenerRegistry.register(
|
|||
const oldGridDimensions = state['features/filmstrip'].tileViewDimensions.gridDimensions;
|
||||
|
||||
if (!equals(gridDimensions, oldGridDimensions)) {
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
store.dispatch(
|
||||
setTileViewDimensions(
|
||||
gridDimensions,
|
||||
{
|
||||
clientHeight,
|
||||
clientWidth
|
||||
},
|
||||
store
|
||||
)
|
||||
);
|
||||
store.dispatch(setTileViewDimensions(gridDimensions));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -53,23 +42,14 @@ StateListenerRegistry.register(
|
|||
const state = store.getState();
|
||||
|
||||
switch (layout) {
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
store.dispatch(
|
||||
setTileViewDimensions(
|
||||
getTileViewGridDimensions(state),
|
||||
{
|
||||
clientHeight,
|
||||
clientWidth
|
||||
},
|
||||
store
|
||||
)
|
||||
);
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
|
||||
store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
|
||||
store.dispatch(setHorizontalViewDimensions());
|
||||
break;
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
store.dispatch(setVerticalViewDimensions());
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
@ -168,17 +148,7 @@ StateListenerRegistry.register(
|
|||
|
||||
if (shouldDisplayTileView(state)) {
|
||||
const gridDimensions = getTileViewGridDimensions(state);
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
store.dispatch(
|
||||
setTileViewDimensions(
|
||||
gridDimensions,
|
||||
{
|
||||
clientHeight,
|
||||
clientWidth
|
||||
},
|
||||
store
|
||||
)
|
||||
);
|
||||
store.dispatch(setTileViewDimensions(gridDimensions));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -106,11 +106,12 @@ export function getTileViewGridDimensions(state: Object) {
|
|||
const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
|
||||
const columns = Math.min(columnsToMaintainASquare, maxColumns);
|
||||
const rows = Math.ceil(numberOfParticipants / columns);
|
||||
const visibleRows = Math.min(maxColumns, rows);
|
||||
const minVisibleRows = Math.min(maxColumns, rows);
|
||||
|
||||
return {
|
||||
columns,
|
||||
visibleRows
|
||||
minVisibleRows,
|
||||
rows
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue