mirror of https://github.com/jitsi/jitsi-meet
commit
195462a1a8
@ -0,0 +1,113 @@ |
||||
/** |
||||
* CSS styles that are specific to the filmstrip that shows the thumbnail tiles. |
||||
*/ |
||||
.tile-view { |
||||
/** |
||||
* Add a border around the active speaker to make the thumbnail easier to |
||||
* see. |
||||
*/ |
||||
.active-speaker { |
||||
border: $thumbnailVideoBorder solid $videoThumbnailSelected; |
||||
box-shadow: inset 0 0 3px $videoThumbnailSelected, |
||||
0 0 3px $videoThumbnailSelected; |
||||
} |
||||
|
||||
#filmstripRemoteVideos { |
||||
align-items: center; |
||||
box-sizing: border-box; |
||||
display: flex; |
||||
flex-direction: column; |
||||
height: 100vh; |
||||
width: 100vw; |
||||
} |
||||
|
||||
.filmstrip__videos .videocontainer { |
||||
&:not(.active-speaker), |
||||
&:hover:not(.active-speaker) { |
||||
border: none; |
||||
box-shadow: none; |
||||
} |
||||
} |
||||
|
||||
#remoteVideos { |
||||
/** |
||||
* Height is modified with an inline style in horizontal filmstrip mode |
||||
* so !important is used to override that. |
||||
*/ |
||||
height: 100% !important; |
||||
width: 100%; |
||||
} |
||||
|
||||
.filmstrip { |
||||
align-items: center; |
||||
display: flex; |
||||
height: 100%; |
||||
justify-content: center; |
||||
left: 0; |
||||
position: fixed; |
||||
top: 0; |
||||
width: 100%; |
||||
z-index: $filmstripVideosZ |
||||
} |
||||
|
||||
/** |
||||
* Regardless of the user setting, do not let the filmstrip be in a hidden |
||||
* state. |
||||
*/ |
||||
.filmstrip__videos.hidden { |
||||
display: block; |
||||
} |
||||
|
||||
#filmstripRemoteVideos { |
||||
box-sizing: border-box; |
||||
|
||||
/** |
||||
* Allow scrolling of the thumbnails. |
||||
*/ |
||||
overflow: 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; |
||||
height: 100vh; |
||||
justify-content: center; |
||||
padding: 100px 0; |
||||
|
||||
.videocontainer { |
||||
box-sizing: border-box; |
||||
display: block; |
||||
margin: 5px; |
||||
} |
||||
|
||||
video { |
||||
object-fit: contain; |
||||
} |
||||
} |
||||
|
||||
.has-overflow#filmstripRemoteVideosContainer { |
||||
align-content: baseline; |
||||
} |
||||
|
||||
.has-overflow .videocontainer { |
||||
align-self: baseline; |
||||
} |
||||
|
||||
/** |
||||
* Firefox flex acts a little differently. To make sure the bottom row of |
||||
* thumbnails is not overlapped by the horizontal toolbar, margin is added |
||||
* to the local thumbnail to keep it from the bottom of the screen. It is |
||||
* assumed the local thumbnail will always be on the bottom row. |
||||
*/ |
||||
.has-overflow #localVideoContainer { |
||||
margin-bottom: 100px !important; |
||||
} |
||||
} |
@ -0,0 +1,47 @@ |
||||
/** |
||||
* Various overrides outside of the filmstrip to style the app to support a |
||||
* tiled thumbnail experience. |
||||
*/ |
||||
.tile-view { |
||||
/** |
||||
* Let the avatar grow with the tile. |
||||
*/ |
||||
.userAvatar { |
||||
max-height: initial; |
||||
max-width: initial; |
||||
} |
||||
|
||||
/** |
||||
* Hide various features that should not be displayed while in tile view. |
||||
*/ |
||||
#dominantSpeaker, |
||||
#filmstripLocalVideoThumbnail, |
||||
#largeVideoElementsContainer, |
||||
#sharedVideo, |
||||
.filmstrip__toolbar { |
||||
display: none; |
||||
} |
||||
|
||||
#localConnectionMessage, |
||||
#remoteConnectionMessage, |
||||
.watermark { |
||||
z-index: $filmstripVideosZ + 1; |
||||
} |
||||
|
||||
/** |
||||
* The follow styling uses !important to override inline styles set with |
||||
* javascript. |
||||
* |
||||
* TODO: These overrides should be more easy to remove and should be removed |
||||
* when the components are in react so their rendering done declaratively, |
||||
* making conditional styling easier to apply. |
||||
*/ |
||||
#largeVideoElementsContainer, |
||||
#remoteConnectionMessage, |
||||
#remotePresenceMessage { |
||||
display: none !important; |
||||
} |
||||
#largeVideoContainer { |
||||
background-color: $defaultBackground !important; |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
/** |
||||
* The type of the action which enables or disables the feature for showing |
||||
* video thumbnails in a two-axis tile view. |
||||
* |
||||
* @returns {{ |
||||
* type: SET_TILE_VIEW, |
||||
* enabled: boolean |
||||
* }} |
||||
*/ |
||||
export const SET_TILE_VIEW = Symbol('SET_TILE_VIEW'); |
@ -0,0 +1,20 @@ |
||||
// @flow
|
||||
|
||||
import { SET_TILE_VIEW } from './actionTypes'; |
||||
|
||||
/** |
||||
* Creates a (redux) action which signals to set the UI layout to be tiled view |
||||
* or not. |
||||
* |
||||
* @param {boolean} enabled - Whether or not tile view should be shown. |
||||
* @returns {{ |
||||
* type: SET_TILE_VIEW, |
||||
* enabled: boolean |
||||
* }} |
||||
*/ |
||||
export function setTileView(enabled: boolean) { |
||||
return { |
||||
type: SET_TILE_VIEW, |
||||
enabled |
||||
}; |
||||
} |
@ -0,0 +1,90 @@ |
||||
// @flow
|
||||
|
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { |
||||
createToolbarEvent, |
||||
sendAnalytics |
||||
} from '../../analytics'; |
||||
import { translate } from '../../base/i18n'; |
||||
import { |
||||
AbstractButton, |
||||
type AbstractButtonProps |
||||
} from '../../base/toolbox'; |
||||
|
||||
import { setTileView } from '../actions'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link TileViewButton}. |
||||
*/ |
||||
type Props = AbstractButtonProps & { |
||||
|
||||
/** |
||||
* Whether or not tile view layout has been enabled as the user preference. |
||||
*/ |
||||
_tileViewEnabled: boolean, |
||||
|
||||
/** |
||||
* Used to dispatch actions from the buttons. |
||||
*/ |
||||
dispatch: Dispatch<*> |
||||
}; |
||||
|
||||
/** |
||||
* Component that renders a toolbar button for toggling the tile layout view. |
||||
* |
||||
* @extends AbstractButton |
||||
*/ |
||||
class TileViewButton<P: Props> extends AbstractButton<P, *> { |
||||
accessibilityLabel = 'toolbar.accessibilityLabel.tileView'; |
||||
iconName = 'icon-tiles-many'; |
||||
toggledIconName = 'icon-tiles-many toggled'; |
||||
tooltip = 'toolbar.tileViewToggle'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button. |
||||
* |
||||
* @override |
||||
* @protected |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
const { _tileViewEnabled, dispatch } = this.props; |
||||
|
||||
sendAnalytics(createToolbarEvent( |
||||
'tileview.button', |
||||
{ |
||||
'is_enabled': _tileViewEnabled |
||||
})); |
||||
|
||||
dispatch(setTileView(!_tileViewEnabled)); |
||||
} |
||||
|
||||
/** |
||||
* Indicates whether this button is in toggled state or not. |
||||
* |
||||
* @override |
||||
* @protected |
||||
* @returns {boolean} |
||||
*/ |
||||
_isToggled() { |
||||
return this.props._tileViewEnabled; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the associated props for the |
||||
* {@code TileViewButton} component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @returns {{ |
||||
* _tileViewEnabled: boolean |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
return { |
||||
_tileViewEnabled: state['features/video-layout'].tileViewEnabled |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(TileViewButton)); |
@ -0,0 +1 @@ |
||||
export { default as TileViewButton } from './TileViewButton'; |
@ -0,0 +1,10 @@ |
||||
/** |
||||
* An enumeration of the different display layouts supported by the application. |
||||
* |
||||
* @type {Object} |
||||
*/ |
||||
export const LAYOUTS = { |
||||
HORIZONTAL_FILMSTRIP_VIEW: 'horizontal-filmstrip-view', |
||||
TILE_VIEW: 'tile-view', |
||||
VERTICAL_FILMSTRIP_VIEW: 'vertical-filmstrip-view' |
||||
}; |
@ -0,0 +1,78 @@ |
||||
// @flow
|
||||
|
||||
import { LAYOUTS } from './constants'; |
||||
|
||||
declare var interfaceConfig: Object; |
||||
|
||||
/** |
||||
* Returns the {@code LAYOUTS} constant associated with the layout |
||||
* the application should currently be in. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @returns {string} |
||||
*/ |
||||
export function getCurrentLayout(state: Object) { |
||||
if (shouldDisplayTileView(state)) { |
||||
return LAYOUTS.TILE_VIEW; |
||||
} else if (interfaceConfig.VERTICAL_FILMSTRIP) { |
||||
return LAYOUTS.VERTICAL_FILMSTRIP_VIEW; |
||||
} |
||||
|
||||
return LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW; |
||||
} |
||||
|
||||
/** |
||||
* Returns how many columns should be displayed in tile view. The number |
||||
* returned will be between 1 and 5, inclusive. |
||||
* |
||||
* @returns {number} |
||||
*/ |
||||
export function getMaxColumnCount() { |
||||
const configuredMax = interfaceConfig.TILE_VIEW_MAX_COLUMNS || 5; |
||||
|
||||
return Math.max(Math.min(configuredMax, 1), 5); |
||||
} |
||||
|
||||
/** |
||||
* Returns the cell count dimensions for tile view. Tile view tries to uphold |
||||
* equal count of tiles for height and width, until maxColumn is reached in |
||||
* which rows will be added but no more columns. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @param {number} maxColumns - The maximum number of columns that can be |
||||
* displayed. |
||||
* @returns {Object} An object is return with the desired number of columns, |
||||
* rows, and visible rows (the rest should overflow) for the tile view layout. |
||||
*/ |
||||
export function getTileViewGridDimensions(state: Object, maxColumns: number) { |
||||
// Purposefully include all participants, which includes fake participants
|
||||
// that should show a thumbnail.
|
||||
const potentialThumbnails = state['features/base/participants'].length; |
||||
|
||||
const columnsToMaintainASquare = Math.ceil(Math.sqrt(potentialThumbnails)); |
||||
const columns = Math.min(columnsToMaintainASquare, maxColumns); |
||||
const rows = Math.ceil(potentialThumbnails / columns); |
||||
const visibleRows = Math.min(maxColumns, rows); |
||||
|
||||
return { |
||||
columns, |
||||
rows, |
||||
visibleRows |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Selector for determining if the UI layout should be in tile view. Tile view |
||||
* is determined by more than just having the tile view setting enabled, as |
||||
* one-on-one calls should not be in tile view, as well as etherpad editing. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @returns {boolean} True if tile view should be displayed. |
||||
*/ |
||||
export function shouldDisplayTileView(state: Object = {}) { |
||||
return Boolean( |
||||
state['features/video-layout'] |
||||
&& state['features/video-layout'].tileViewEnabled |
||||
&& !state['features/etherpad'].editing |
||||
); |
||||
} |
@ -1 +1,9 @@ |
||||
export * from './actions'; |
||||
export * from './actionTypes'; |
||||
export * from './components'; |
||||
export * from './constants'; |
||||
export * from './functions'; |
||||
|
||||
import './middleware'; |
||||
import './reducer'; |
||||
import './subscriber'; |
||||
|
@ -0,0 +1,17 @@ |
||||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux'; |
||||
|
||||
import { SET_TILE_VIEW } from './actionTypes'; |
||||
|
||||
ReducerRegistry.register('features/video-layout', (state = {}, action) => { |
||||
switch (action.type) { |
||||
case SET_TILE_VIEW: |
||||
return { |
||||
...state, |
||||
tileViewEnabled: action.enabled |
||||
}; |
||||
} |
||||
|
||||
return state; |
||||
}); |
@ -0,0 +1,24 @@ |
||||
// @flow
|
||||
|
||||
import { |
||||
VIDEO_QUALITY_LEVELS, |
||||
setMaxReceiverVideoQuality |
||||
} from '../base/conference'; |
||||
import { StateListenerRegistry } from '../base/redux'; |
||||
import { selectParticipant } from '../large-video'; |
||||
import { shouldDisplayTileView } from './functions'; |
||||
|
||||
/** |
||||
* StateListenerRegistry provides a reliable way of detecting changes to |
||||
* preferred layout state and dispatching additional actions. |
||||
*/ |
||||
StateListenerRegistry.register( |
||||
/* selector */ state => shouldDisplayTileView(state), |
||||
/* listener */ (displayTileView, { dispatch }) => { |
||||
dispatch(selectParticipant()); |
||||
|
||||
if (!displayTileView) { |
||||
dispatch(setMaxReceiverVideoQuality(VIDEO_QUALITY_LEVELS.HIGH)); |
||||
} |
||||
} |
||||
); |
Loading…
Reference in new issue