You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TileView.js 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. // @flow
  2. import React, { Component } from 'react';
  3. import {
  4. ScrollView,
  5. TouchableWithoutFeedback,
  6. View
  7. } from 'react-native';
  8. import { connect } from 'react-redux';
  9. import type { Dispatch } from 'redux';
  10. import {
  11. getNearestReceiverVideoQualityLevel,
  12. setMaxReceiverVideoQuality
  13. } from '../../../base/conference';
  14. import {
  15. DimensionsDetector,
  16. isNarrowAspectRatio,
  17. makeAspectRatioAware
  18. } from '../../../base/responsive-ui';
  19. import Thumbnail from './Thumbnail';
  20. import styles from './styles';
  21. /**
  22. * The type of the React {@link Component} props of {@link TileView}.
  23. */
  24. type Props = {
  25. /**
  26. * The participants in the conference.
  27. */
  28. _participants: Array<Object>,
  29. /**
  30. * Invoked to update the receiver video quality.
  31. */
  32. dispatch: Dispatch<any>,
  33. /**
  34. * Callback to invoke when tile view is tapped.
  35. */
  36. onClick: Function
  37. };
  38. /**
  39. * The type of the React {@link Component} state of {@link TileView}.
  40. */
  41. type State = {
  42. /**
  43. * The available width for {@link TileView} to occupy.
  44. */
  45. height: number,
  46. /**
  47. * The available height for {@link TileView} to occupy.
  48. */
  49. width: number
  50. };
  51. /**
  52. * The margin for each side of the tile view. Taken away from the available
  53. * height and width for the tile container to display in.
  54. *
  55. * @private
  56. * @type {number}
  57. */
  58. const MARGIN = 10;
  59. /**
  60. * The aspect ratio the tiles should display in.
  61. *
  62. * @private
  63. * @type {number}
  64. */
  65. const TILE_ASPECT_RATIO = 1;
  66. /**
  67. * Implements a React {@link Component} which displays thumbnails in a two
  68. * dimensional grid.
  69. *
  70. * @extends Component
  71. */
  72. class TileView extends Component<Props, State> {
  73. state = {
  74. height: 0,
  75. width: 0
  76. };
  77. /**
  78. * Initializes a new {@code TileView} instance.
  79. *
  80. * @param {Object} props - The read-only properties with which the new
  81. * instance is to be initialized.
  82. */
  83. constructor(props: Props) {
  84. super(props);
  85. // Bind event handler so it is only bound once per instance.
  86. this._onDimensionsChanged = this._onDimensionsChanged.bind(this);
  87. }
  88. /**
  89. * Implements React's {@link Component#componentDidMount}.
  90. *
  91. * @inheritdoc
  92. */
  93. componentDidMount() {
  94. this._updateReceiverQuality();
  95. }
  96. /**
  97. * Implements React's {@link Component#componentDidUpdate}.
  98. *
  99. * @inheritdoc
  100. */
  101. componentDidUpdate() {
  102. this._updateReceiverQuality();
  103. }
  104. /**
  105. * Implements React's {@link Component#render()}.
  106. *
  107. * @inheritdoc
  108. * @returns {ReactElement}
  109. */
  110. render() {
  111. const { onClick } = this.props;
  112. const { height, width } = this.state;
  113. const rowElements = this._groupIntoRows(
  114. this._renderThumbnails(), this._getColumnCount());
  115. return (
  116. <DimensionsDetector
  117. onDimensionsChanged = { this._onDimensionsChanged }>
  118. <ScrollView
  119. style = {{
  120. ...styles.tileView,
  121. height,
  122. width
  123. }}>
  124. <TouchableWithoutFeedback onPress = { onClick }>
  125. <View
  126. style = {{
  127. ...styles.tileViewRows,
  128. minHeight: height,
  129. minWidth: width
  130. }}>
  131. { rowElements }
  132. </View>
  133. </TouchableWithoutFeedback>
  134. </ScrollView>
  135. </DimensionsDetector>
  136. );
  137. }
  138. /**
  139. * Returns how many columns should be displayed for tile view.
  140. *
  141. * @returns {number}
  142. * @private
  143. */
  144. _getColumnCount() {
  145. const participantCount = this.props._participants.length;
  146. // For narrow view, tiles should stack on top of each other for a lonely
  147. // call and a 1:1 call. Otherwise tiles should be grouped into rows of
  148. // two.
  149. if (isNarrowAspectRatio(this)) {
  150. return participantCount >= 3 ? 2 : 1;
  151. }
  152. if (participantCount === 4) {
  153. // In wide view, a four person call should display as a 2x2 grid.
  154. return 2;
  155. }
  156. return Math.min(3, participantCount);
  157. }
  158. /**
  159. * Returns all participants with the local participant at the end.
  160. *
  161. * @private
  162. * @returns {Participant[]}
  163. */
  164. _getSortedParticipants() {
  165. const participants = [];
  166. let localParticipant;
  167. for (const participant of this.props._participants) {
  168. if (participant.local) {
  169. localParticipant = participant;
  170. } else {
  171. participants.push(participant);
  172. }
  173. }
  174. localParticipant && participants.push(localParticipant);
  175. return participants;
  176. }
  177. /**
  178. * Calculate the height and width for the tiles.
  179. *
  180. * @private
  181. * @returns {Object}
  182. */
  183. _getTileDimensions() {
  184. const { _participants } = this.props;
  185. const { height, width } = this.state;
  186. const columns = this._getColumnCount();
  187. const participantCount = _participants.length;
  188. const heightToUse = height - (MARGIN * 2);
  189. const widthToUse = width - (MARGIN * 2);
  190. let tileWidth;
  191. // If there is going to be at least two rows, ensure that at least two
  192. // rows display fully on screen.
  193. if (participantCount / columns > 1) {
  194. tileWidth
  195. = Math.min(widthToUse / columns, heightToUse / 2);
  196. } else {
  197. tileWidth = Math.min(widthToUse / columns, heightToUse);
  198. }
  199. return {
  200. height: tileWidth / TILE_ASPECT_RATIO,
  201. width: tileWidth
  202. };
  203. }
  204. /**
  205. * Splits a list of thumbnails into React Elements with a maximum of
  206. * {@link rowLength} thumbnails in each.
  207. *
  208. * @param {Array} thumbnails - The list of thumbnails that should be split
  209. * into separate row groupings.
  210. * @param {number} rowLength - How many thumbnails should be in each row.
  211. * @private
  212. * @returns {ReactElement[]}
  213. */
  214. _groupIntoRows(thumbnails, rowLength) {
  215. const rowElements = [];
  216. for (let i = 0; i < thumbnails.length; i++) {
  217. if (i % rowLength === 0) {
  218. const thumbnailsInRow
  219. = thumbnails.slice(i, i + rowLength);
  220. rowElements.push(
  221. <View
  222. key = { rowElements.length }
  223. style = { styles.tileViewRow }>
  224. { thumbnailsInRow }
  225. </View>
  226. );
  227. }
  228. }
  229. return rowElements;
  230. }
  231. _onDimensionsChanged: (width: number, height: number) => void;
  232. /**
  233. * Updates the known available state for {@link TileView} to occupy.
  234. *
  235. * @param {number} width - The component's current width.
  236. * @param {number} height - The component's current height.
  237. * @private
  238. * @returns {void}
  239. */
  240. _onDimensionsChanged(width: number, height: number) {
  241. this.setState({
  242. height,
  243. width
  244. });
  245. }
  246. /**
  247. * Creates React Elements to display each participant in a thumbnail. Each
  248. * tile will be.
  249. *
  250. * @private
  251. * @returns {ReactElement[]}
  252. */
  253. _renderThumbnails() {
  254. const styleOverrides = {
  255. aspectRatio: TILE_ASPECT_RATIO,
  256. flex: 0,
  257. height: this._getTileDimensions().height,
  258. width: null
  259. };
  260. return this._getSortedParticipants()
  261. .map(participant => (
  262. <Thumbnail
  263. disablePin = { true }
  264. disableTint = { true }
  265. key = { participant.id }
  266. participant = { participant }
  267. styleOverrides = { styleOverrides } />));
  268. }
  269. /**
  270. * Sets the receiver video quality based on the dimensions of the thumbnails
  271. * that are displayed.
  272. *
  273. * @private
  274. * @returns {void}
  275. */
  276. _updateReceiverQuality() {
  277. const { height } = this._getTileDimensions();
  278. const qualityLevel = getNearestReceiverVideoQualityLevel(height);
  279. this.props.dispatch(setMaxReceiverVideoQuality(qualityLevel));
  280. }
  281. }
  282. /**
  283. * Maps (parts of) the redux state to the associated {@code TileView}'s props.
  284. *
  285. * @param {Object} state - The redux state.
  286. * @private
  287. * @returns {{
  288. * _participants: Participant[]
  289. * }}
  290. */
  291. function _mapStateToProps(state) {
  292. return {
  293. _participants: state['features/base/participants']
  294. };
  295. }
  296. // $FlowExpectedError
  297. export default connect(_mapStateToProps)(makeAspectRatioAware(TileView));