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.

Conference.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. // @flow
  2. import React from 'react';
  3. import { BackHandler, SafeAreaView, StatusBar, View } from 'react-native';
  4. import { appNavigate } from '../../../app';
  5. import { connect, disconnect } from '../../../base/connection';
  6. import { getParticipantCount } from '../../../base/participants';
  7. import { Container, LoadingIndicator, TintedView } from '../../../base/react';
  8. import { connect as reactReduxConnect } from '../../../base/redux';
  9. import {
  10. isNarrowAspectRatio,
  11. makeAspectRatioAware
  12. } from '../../../base/responsive-ui';
  13. import { TestConnectionInfo } from '../../../base/testing';
  14. import { createDesiredLocalTracks } from '../../../base/tracks';
  15. import { ConferenceNotification } from '../../../calendar-sync';
  16. import { Chat } from '../../../chat';
  17. import {
  18. FILMSTRIP_SIZE,
  19. Filmstrip,
  20. isFilmstripVisible,
  21. TileView
  22. } from '../../../filmstrip';
  23. import { LargeVideo } from '../../../large-video';
  24. import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
  25. import { Captions } from '../../../subtitles';
  26. import { setToolboxVisible, Toolbox } from '../../../toolbox';
  27. import {
  28. AbstractConference,
  29. abstractMapStateToProps
  30. } from '../AbstractConference';
  31. import DisplayNameLabel from './DisplayNameLabel';
  32. import Labels from './Labels';
  33. import NavigationBar from './NavigationBar';
  34. import styles from './styles';
  35. import type { AbstractProps } from '../AbstractConference';
  36. /**
  37. * The type of the React {@code Component} props of {@link Conference}.
  38. */
  39. type Props = AbstractProps & {
  40. /**
  41. * The indicator which determines that we are still connecting to the
  42. * conference which includes establishing the XMPP connection and then
  43. * joining the room. If truthy, then an activity/loading indicator will be
  44. * rendered.
  45. *
  46. * @private
  47. */
  48. _connecting: boolean,
  49. /**
  50. * Set to {@code true} when the filmstrip is currently visible.
  51. *
  52. * @private
  53. */
  54. _filmstripVisible: boolean,
  55. /**
  56. * Current conference's full URL.
  57. *
  58. * @private
  59. */
  60. _locationURL: URL,
  61. /**
  62. * The handler which dispatches the (redux) action connect.
  63. *
  64. * @private
  65. * @returns {void}
  66. */
  67. _onConnect: Function,
  68. /**
  69. * The handler which dispatches the (redux) action disconnect.
  70. *
  71. * @private
  72. * @returns {void}
  73. */
  74. _onDisconnect: Function,
  75. /**
  76. * Handles a hardware button press for back navigation. Leaves the
  77. * associated {@code Conference}.
  78. *
  79. * @private
  80. * @returns {boolean} As the associated conference is unconditionally left
  81. * and exiting the app while it renders a {@code Conference} is undesired,
  82. * {@code true} is always returned.
  83. */
  84. _onHardwareBackPress: Function,
  85. /**
  86. * The number of participants in the conference.
  87. *
  88. * @private
  89. */
  90. _participantCount: number,
  91. /**
  92. * The indicator which determines whether the UI is reduced (to accommodate
  93. * smaller display areas).
  94. *
  95. * @private
  96. */
  97. _reducedUI: boolean,
  98. /**
  99. * The handler which dispatches the (redux) action {@link setToolboxVisible}
  100. * to show/hide the {@link Toolbox}.
  101. *
  102. * @param {boolean} visible - {@code true} to show the {@code Toolbox} or
  103. * {@code false} to hide it.
  104. * @private
  105. * @returns {void}
  106. */
  107. _setToolboxVisible: Function,
  108. /**
  109. * The indicator which determines whether the Toolbox is visible.
  110. *
  111. * @private
  112. */
  113. _toolboxVisible: boolean,
  114. /**
  115. * The indicator which determines whether the Toolbox is always visible.
  116. *
  117. * @private
  118. */
  119. _toolboxAlwaysVisible: boolean
  120. };
  121. /**
  122. * The conference page of the mobile (i.e. React Native) application.
  123. */
  124. class Conference extends AbstractConference<Props, *> {
  125. /**
  126. * Initializes a new Conference instance.
  127. *
  128. * @param {Object} props - The read-only properties with which the new
  129. * instance is to be initialized.
  130. */
  131. constructor(props) {
  132. super(props);
  133. // Bind event handlers so they are only bound once per instance.
  134. this._onClick = this._onClick.bind(this);
  135. }
  136. /**
  137. * Implements {@link Component#componentDidMount()}. Invoked immediately
  138. * after this component is mounted.
  139. *
  140. * @inheritdoc
  141. * @returns {void}
  142. */
  143. componentDidMount() {
  144. this.props._onConnect();
  145. BackHandler.addEventListener(
  146. 'hardwareBackPress',
  147. this.props._onHardwareBackPress);
  148. // Show the toolbox if we are the only participant; otherwise, the whole
  149. // UI looks too unpopulated the LargeVideo visible.
  150. const { _participantCount, _setToolboxVisible } = this.props;
  151. _participantCount === 1 && _setToolboxVisible(true);
  152. }
  153. /**
  154. * Implements React's {@link Component#componentDidUpdate()}.
  155. *
  156. * @inheritdoc
  157. */
  158. componentDidUpdate(pevProps: Props) {
  159. const {
  160. _locationURL: oldLocationURL,
  161. _participantCount: oldParticipantCount,
  162. _room: oldRoom
  163. } = pevProps;
  164. const {
  165. _locationURL: newLocationURL,
  166. _participantCount: newParticipantCount,
  167. _room: newRoom,
  168. _setToolboxVisible,
  169. _toolboxVisible
  170. } = this.props;
  171. // If the location URL changes we need to reconnect.
  172. oldLocationURL !== newLocationURL && this.props._onDisconnect();
  173. // Start the connection process when there is a (valid) room.
  174. oldRoom !== newRoom && newRoom && this.props._onConnect();
  175. if (oldParticipantCount === 1
  176. && newParticipantCount > 1
  177. && _toolboxVisible) {
  178. _setToolboxVisible(false);
  179. } else if (oldParticipantCount > 1
  180. && newParticipantCount === 1
  181. && !_toolboxVisible) {
  182. _setToolboxVisible(true);
  183. }
  184. }
  185. /**
  186. * Implements {@link Component#componentWillUnmount()}. Invoked immediately
  187. * before this component is unmounted and destroyed. Disconnects the
  188. * conference described by the redux store/state.
  189. *
  190. * @inheritdoc
  191. * @returns {void}
  192. */
  193. componentWillUnmount() {
  194. // Tear handling any hardware button presses for back navigation down.
  195. BackHandler.removeEventListener(
  196. 'hardwareBackPress',
  197. this.props._onHardwareBackPress);
  198. this.props._onDisconnect();
  199. }
  200. /**
  201. * Implements React's {@link Component#render()}.
  202. *
  203. * @inheritdoc
  204. * @returns {ReactElement}
  205. */
  206. render() {
  207. const {
  208. _connecting,
  209. _reducedUI,
  210. _shouldDisplayTileView
  211. } = this.props;
  212. return (
  213. <Container style = { styles.conference }>
  214. <StatusBar
  215. barStyle = 'light-content'
  216. hidden = { true }
  217. translucent = { true } />
  218. <Chat />
  219. <AddPeopleDialog />
  220. {/*
  221. * The LargeVideo is the lowermost stacking layer.
  222. */
  223. _shouldDisplayTileView
  224. ? <TileView onClick = { this._onClick } />
  225. : <LargeVideo onClick = { this._onClick } />
  226. }
  227. {/*
  228. * If there is a ringing call, show the callee's info.
  229. */
  230. _reducedUI || <CalleeInfoContainer />
  231. }
  232. {/*
  233. * The activity/loading indicator goes above everything, except
  234. * the toolbox/toolbars and the dialogs.
  235. */
  236. _connecting
  237. && <TintedView>
  238. <LoadingIndicator />
  239. </TintedView>
  240. }
  241. <View
  242. pointerEvents = 'box-none'
  243. style = { styles.toolboxAndFilmstripContainer }>
  244. <Labels />
  245. <Captions onPress = { this._onClick } />
  246. <DisplayNameLabel />
  247. {/*
  248. * The Toolbox is in a stacking layer bellow the Filmstrip.
  249. */}
  250. <Toolbox />
  251. {/*
  252. * The Filmstrip is in a stacking layer above the
  253. * LargeVideo. The LargeVideo and the Filmstrip form what
  254. * the Web/React app calls "videospace". Presumably, the
  255. * name and grouping stem from the fact that these two
  256. * React Components depict the videos of the conference's
  257. * participants.
  258. */
  259. _shouldDisplayTileView ? undefined : <Filmstrip />
  260. }
  261. </View>
  262. <SafeAreaView
  263. pointerEvents = 'box-none'
  264. style = { styles.navBarSafeView }>
  265. <NavigationBar />
  266. { this.renderNotificationsContainer() }
  267. </SafeAreaView>
  268. <TestConnectionInfo />
  269. {
  270. this._renderConferenceNotification()
  271. }
  272. </Container>
  273. );
  274. }
  275. _onClick: () => void;
  276. /**
  277. * Changes the value of the toolboxVisible state, thus allowing us to switch
  278. * between Toolbox and Filmstrip and change their visibility.
  279. *
  280. * @private
  281. * @returns {void}
  282. */
  283. _onClick() {
  284. if (this.props._toolboxAlwaysVisible) {
  285. return;
  286. }
  287. const toolboxVisible = !this.props._toolboxVisible;
  288. this.props._setToolboxVisible(toolboxVisible);
  289. }
  290. /**
  291. * Renders the conference notification badge if the feature is enabled.
  292. *
  293. * @private
  294. * @returns {React$Node}
  295. */
  296. _renderConferenceNotification() {
  297. // XXX If the calendar feature is disabled on a platform, then we don't
  298. // have its components exported so an undefined check is necessary.
  299. return (
  300. !this.props._reducedUI && ConferenceNotification
  301. ? <ConferenceNotification />
  302. : undefined);
  303. }
  304. /**
  305. * Renders a container for notifications to be displayed by the
  306. * base/notifications feature.
  307. *
  308. * @private
  309. * @returns {React$Element}
  310. */
  311. renderNotificationsContainer() {
  312. const notificationsStyle = {};
  313. // In the landscape mode (wide) there's problem with notifications being
  314. // shadowed by the filmstrip rendered on the right. This makes the "x"
  315. // button not clickable. In order to avoid that a margin of the
  316. // filmstrip's size is added to the right.
  317. //
  318. // Pawel: after many attempts I failed to make notifications adjust to
  319. // their contents width because of column and rows being used in the
  320. // flex layout. The only option that seemed to limit the notification's
  321. // size was explicit 'width' value which is not better than the margin
  322. // added here.
  323. if (this.props._filmstripVisible && !isNarrowAspectRatio(this)) {
  324. notificationsStyle.marginRight = FILMSTRIP_SIZE;
  325. }
  326. return super.renderNotificationsContainer(
  327. {
  328. style: notificationsStyle
  329. }
  330. );
  331. }
  332. }
  333. /**
  334. * Maps dispatching of some action to React component props.
  335. *
  336. * @param {Function} dispatch - Redux action dispatcher.
  337. * @private
  338. * @returns {{
  339. * _onConnect: Function,
  340. * _onDisconnect: Function,
  341. * _onHardwareBackPress: Function,
  342. * _setToolboxVisible: Function
  343. * }}
  344. */
  345. function _mapDispatchToProps(dispatch) {
  346. return {
  347. /**
  348. * Dispatches actions to create the desired local tracks and for
  349. * connecting to the conference.
  350. *
  351. * @private
  352. * @returns {void}
  353. */
  354. _onConnect() {
  355. dispatch(createDesiredLocalTracks());
  356. dispatch(connect());
  357. },
  358. /**
  359. * Dispatches an action disconnecting from the conference.
  360. *
  361. * @private
  362. * @returns {void}
  363. */
  364. _onDisconnect() {
  365. dispatch(disconnect());
  366. },
  367. /**
  368. * Handles a hardware button press for back navigation. Leaves the
  369. * associated {@code Conference}.
  370. *
  371. * @returns {boolean} As the associated conference is unconditionally
  372. * left and exiting the app while it renders a {@code Conference} is
  373. * undesired, {@code true} is always returned.
  374. */
  375. _onHardwareBackPress() {
  376. dispatch(appNavigate(undefined));
  377. return true;
  378. },
  379. /**
  380. * Dispatches an action changing the visibility of the {@link Toolbox}.
  381. *
  382. * @private
  383. * @param {boolean} visible - Pass {@code true} to show the
  384. * {@code Toolbox} or {@code false} to hide it.
  385. * @returns {void}
  386. */
  387. _setToolboxVisible(visible) {
  388. dispatch(setToolboxVisible(visible));
  389. }
  390. };
  391. }
  392. /**
  393. * Maps (parts of) the redux state to the associated {@code Conference}'s props.
  394. *
  395. * @param {Object} state - The redux state.
  396. * @private
  397. * @returns {Props}
  398. */
  399. function _mapStateToProps(state) {
  400. const { connecting, connection, locationURL }
  401. = state['features/base/connection'];
  402. const {
  403. conference,
  404. joining,
  405. leaving
  406. } = state['features/base/conference'];
  407. const { reducedUI } = state['features/base/responsive-ui'];
  408. const { alwaysVisible, visible } = state['features/toolbox'];
  409. // XXX There is a window of time between the successful establishment of the
  410. // XMPP connection and the subsequent commencement of joining the MUC during
  411. // which the app does not appear to be doing anything according to the redux
  412. // state. In order to not toggle the _connecting props during the window of
  413. // time in question, define _connecting as follows:
  414. // - the XMPP connection is connecting, or
  415. // - the XMPP connection is connected and the conference is joining, or
  416. // - the XMPP connection is connected and we have no conference yet, nor we
  417. // are leaving one.
  418. const connecting_
  419. = connecting || (connection && (joining || (!conference && !leaving)));
  420. return {
  421. ...abstractMapStateToProps(state),
  422. /**
  423. * The indicator which determines that we are still connecting to the
  424. * conference which includes establishing the XMPP connection and then
  425. * joining the room. If truthy, then an activity/loading indicator will
  426. * be rendered.
  427. *
  428. * @private
  429. * @type {boolean}
  430. */
  431. _connecting: Boolean(connecting_),
  432. /**
  433. * Is {@code true} when the filmstrip is currently visible.
  434. */
  435. _filmstripVisible: isFilmstripVisible(state),
  436. /**
  437. * Current conference's full URL.
  438. *
  439. * @private
  440. * @type {URL}
  441. */
  442. _locationURL: locationURL,
  443. /**
  444. * The number of participants in the conference.
  445. *
  446. * @private
  447. * @type {number}
  448. */
  449. _participantCount: getParticipantCount(state),
  450. /**
  451. * The indicator which determines whether the UI is reduced (to
  452. * accommodate smaller display areas).
  453. *
  454. * @private
  455. * @type {boolean}
  456. */
  457. _reducedUI: reducedUI,
  458. /**
  459. * The indicator which determines whether the Toolbox is visible.
  460. *
  461. * @private
  462. * @type {boolean}
  463. */
  464. _toolboxVisible: visible,
  465. /**
  466. * The indicator which determines whether the Toolbox is always visible.
  467. *
  468. * @private
  469. * @type {boolean}
  470. */
  471. _toolboxAlwaysVisible: alwaysVisible
  472. };
  473. }
  474. export default reactReduxConnect(_mapStateToProps, _mapDispatchToProps)(
  475. makeAspectRatioAware(Conference));