mirror of https://github.com/jitsi/jitsi-meet
* feat(tile-view): initial implementation for mobile - Create a tile view component for displaying thumbnails in a two-dimensional grid. - Update the existing TileViewButton so it shows a label in the overflow menu. - Modify conference so it can display TileView while hiding Filmstrip. - Modify Thumbnail so its width/height can be set and to prevent pinning while in tile view mode. * use style array for thumbnail styles * change ternary to math.min for expressiveness * use dimensiondetector * pass explicit disableTint prop * use makeAspectRatioAware instead of aspectRatio prop * update docs * fix docs again (fix laziest copy/paste job I've ever done) * large-video: rename onPress prop to onClick * change forEach to for...of * use truthy check fallthrough logic instead of explicit if * put tile view button second to last in menu * move spacer to a constant * the magical incantation to make flow shut uppull/3447/merge jitsi-meet_3337
parent
37ff77cd5b
commit
c25d6eb9a8
@ -0,0 +1,337 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
import { |
||||
ScrollView, |
||||
TouchableWithoutFeedback, |
||||
View |
||||
} from 'react-native'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { |
||||
getNearestReceiverVideoQualityLevel, |
||||
setMaxReceiverVideoQuality |
||||
} from '../../../base/conference'; |
||||
import { |
||||
DimensionsDetector, |
||||
isNarrowAspectRatio, |
||||
makeAspectRatioAware |
||||
} from '../../../base/responsive-ui'; |
||||
|
||||
import Thumbnail from './Thumbnail'; |
||||
import styles from './styles'; |
||||
|
||||
/** |
||||
* The type of the React {@link Component} props of {@link TileView}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The participants in the conference. |
||||
*/ |
||||
_participants: Array<Object>, |
||||
|
||||
/** |
||||
* Invoked to update the receiver video quality. |
||||
*/ |
||||
dispatch: Dispatch<*>, |
||||
|
||||
/** |
||||
* Callback to invoke when tile view is tapped. |
||||
*/ |
||||
onClick: Function |
||||
}; |
||||
|
||||
/** |
||||
* The type of the React {@link Component} state of {@link TileView}. |
||||
*/ |
||||
type State = { |
||||
|
||||
/** |
||||
* The available width for {@link TileView} to occupy. |
||||
*/ |
||||
height: number, |
||||
|
||||
/** |
||||
* The available height for {@link TileView} to occupy. |
||||
*/ |
||||
width: number |
||||
}; |
||||
|
||||
/** |
||||
* The margin for each side of the tile view. Taken away from the available |
||||
* height and width for the tile container to display in. |
||||
* |
||||
* @private |
||||
* @type {number} |
||||
*/ |
||||
const MARGIN = 10; |
||||
|
||||
/** |
||||
* The aspect ratio the tiles should display in. |
||||
* |
||||
* @private |
||||
* @type {number} |
||||
*/ |
||||
const TILE_ASPECT_RATIO = 1; |
||||
|
||||
/** |
||||
* Implements a React {@link Component} which displays thumbnails in a two |
||||
* dimensional grid. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class TileView extends Component<Props, State> { |
||||
state = { |
||||
height: 0, |
||||
width: 0 |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code TileView} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handler so it is only bound once per instance.
|
||||
this._onDimensionsChanged = this._onDimensionsChanged.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidMount}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
this._updateReceiverQuality(); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidUpdate}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidUpdate() { |
||||
this._updateReceiverQuality(); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { onClick } = this.props; |
||||
const { height, width } = this.state; |
||||
const rowElements = this._groupIntoRows( |
||||
this._renderThumbnails(), this._getColumnCount()); |
||||
|
||||
return ( |
||||
<DimensionsDetector |
||||
onDimensionsChanged = { this._onDimensionsChanged }> |
||||
<ScrollView |
||||
style = {{ |
||||
...styles.tileView, |
||||
height, |
||||
width |
||||
}}> |
||||
<TouchableWithoutFeedback onPress = { onClick }> |
||||
<View |
||||
style = {{ |
||||
...styles.tileViewRows, |
||||
minHeight: height, |
||||
minWidth: width |
||||
}}> |
||||
{ rowElements } |
||||
</View> |
||||
</TouchableWithoutFeedback> |
||||
</ScrollView> |
||||
</DimensionsDetector> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Returns how many columns should be displayed for tile view. |
||||
* |
||||
* @returns {number} |
||||
* @private |
||||
*/ |
||||
_getColumnCount() { |
||||
const participantCount = this.props._participants.length; |
||||
|
||||
// For narrow view, tiles should stack on top of each other for a lonely
|
||||
// call and a 1:1 call. Otherwise tiles should be grouped into rows of
|
||||
// two.
|
||||
if (isNarrowAspectRatio(this)) { |
||||
return participantCount >= 3 ? 2 : 1; |
||||
} |
||||
|
||||
if (participantCount === 4) { |
||||
// In wide view, a four person call should display as a 2x2 grid.
|
||||
return 2; |
||||
} |
||||
|
||||
return Math.min(3, participantCount); |
||||
} |
||||
|
||||
/** |
||||
* Returns all participants with the local participant at the end. |
||||
* |
||||
* @private |
||||
* @returns {Participant[]} |
||||
*/ |
||||
_getSortedParticipants() { |
||||
const participants = []; |
||||
let localParticipant; |
||||
|
||||
for (const participant of this.props._participants) { |
||||
if (participant.local) { |
||||
localParticipant = participant; |
||||
} else { |
||||
participants.push(participant); |
||||
} |
||||
} |
||||
|
||||
localParticipant && participants.push(localParticipant); |
||||
|
||||
return participants; |
||||
} |
||||
|
||||
/** |
||||
* Calculate the height and width for the tiles. |
||||
* |
||||
* @private |
||||
* @returns {Object} |
||||
*/ |
||||
_getTileDimensions() { |
||||
const { _participants } = this.props; |
||||
const { height, width } = this.state; |
||||
const columns = this._getColumnCount(); |
||||
const participantCount = _participants.length; |
||||
const heightToUse = height - (MARGIN * 2); |
||||
const widthToUse = width - (MARGIN * 2); |
||||
let tileWidth; |
||||
|
||||
// If there is going to be at least two rows, ensure that at least two
|
||||
// rows display fully on screen.
|
||||
if (participantCount / columns > 1) { |
||||
tileWidth |
||||
= Math.min(widthToUse / columns, heightToUse / 2); |
||||
} else { |
||||
tileWidth = Math.min(widthToUse / columns, heightToUse); |
||||
} |
||||
|
||||
return { |
||||
height: tileWidth / TILE_ASPECT_RATIO, |
||||
width: tileWidth |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Splits a list of thumbnails into React Elements with a maximum of |
||||
* {@link rowLength} thumbnails in each. |
||||
* |
||||
* @param {Array} thumbnails - The list of thumbnails that should be split |
||||
* into separate row groupings. |
||||
* @param {number} rowLength - How many thumbnails should be in each row. |
||||
* @private |
||||
* @returns {ReactElement[]} |
||||
*/ |
||||
_groupIntoRows(thumbnails, rowLength) { |
||||
const rowElements = []; |
||||
|
||||
for (let i = 0; i < thumbnails.length; i++) { |
||||
if (i % rowLength === 0) { |
||||
const thumbnailsInRow |
||||
= thumbnails.slice(i, i + rowLength); |
||||
|
||||
rowElements.push( |
||||
<View |
||||
key = { rowElements.length } |
||||
style = { styles.tileViewRow }> |
||||
{ thumbnailsInRow } |
||||
</View> |
||||
); |
||||
} |
||||
} |
||||
|
||||
return rowElements; |
||||
} |
||||
|
||||
_onDimensionsChanged: (width: number, height: number) => void; |
||||
|
||||
/** |
||||
* Updates the known available state for {@link TileView} to occupy. |
||||
* |
||||
* @param {number} width - The component's current width. |
||||
* @param {number} height - The component's current height. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onDimensionsChanged(width: number, height: number) { |
||||
this.setState({ |
||||
height, |
||||
width |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Creates React Elements to display each participant in a thumbnail. Each |
||||
* tile will be. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement[]} |
||||
*/ |
||||
_renderThumbnails() { |
||||
const styleOverrides = { |
||||
aspectRatio: TILE_ASPECT_RATIO, |
||||
flex: 0, |
||||
height: this._getTileDimensions().height, |
||||
width: null |
||||
}; |
||||
|
||||
return this._getSortedParticipants() |
||||
.map(participant => ( |
||||
<Thumbnail |
||||
disablePin = { true } |
||||
disableTint = { true } |
||||
key = { participant.id } |
||||
participant = { participant } |
||||
styleOverrides = { styleOverrides } />)); |
||||
} |
||||
|
||||
/** |
||||
* Sets the receiver video quality based on the dimensions of the thumbnails |
||||
* that are displayed. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_updateReceiverQuality() { |
||||
const { height } = this._getTileDimensions(); |
||||
const qualityLevel = getNearestReceiverVideoQualityLevel(height); |
||||
|
||||
this.props.dispatch(setMaxReceiverVideoQuality(qualityLevel)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the associated {@code TileView}'s props. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* _participants: Participant[] |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
return { |
||||
_participants: state['features/base/participants'] |
||||
}; |
||||
} |
||||
|
||||
export default connect(_mapStateToProps)(makeAspectRatioAware(TileView)); |
||||
@ -1,2 +1,3 @@ |
||||
export { default as Filmstrip } from './Filmstrip'; |
||||
export { default as TileView } from './TileView'; |
||||
export { default as styles } from './styles'; |
||||
|
||||
Loading…
Reference in new issue