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.6KB

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