Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

Conference.tsx 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. import { useFocusEffect } from '@react-navigation/native';
  2. import React, { useCallback } from 'react';
  3. import {
  4. BackHandler,
  5. NativeModules,
  6. Platform,
  7. SafeAreaView,
  8. StatusBar,
  9. View,
  10. ViewStyle
  11. } from 'react-native';
  12. import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
  13. import { connect, useDispatch } from 'react-redux';
  14. import { appNavigate } from '../../../app/actions';
  15. import { IReduxState, IStore } from '../../../app/types';
  16. import { CONFERENCE_BLURRED, CONFERENCE_FOCUSED } from '../../../base/conference/actionTypes';
  17. import { FULLSCREEN_ENABLED, PIP_ENABLED } from '../../../base/flags/constants';
  18. import { getFeatureFlag } from '../../../base/flags/functions';
  19. import { getParticipantCount } from '../../../base/participants/functions';
  20. import Container from '../../../base/react/components/native/Container';
  21. import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
  22. import TintedView from '../../../base/react/components/native/TintedView';
  23. import {
  24. ASPECT_RATIO_NARROW,
  25. ASPECT_RATIO_WIDE
  26. } from '../../../base/responsive-ui/constants';
  27. import { StyleType } from '../../../base/styles/functions.any';
  28. import TestConnectionInfo from '../../../base/testing/components/TestConnectionInfo';
  29. import { isCalendarEnabled } from '../../../calendar-sync/functions.native';
  30. import DisplayNameLabel from '../../../display-name/components/native/DisplayNameLabel';
  31. import BrandingImageBackground from '../../../dynamic-branding/components/native/BrandingImageBackground';
  32. import Filmstrip from '../../../filmstrip/components/native/Filmstrip';
  33. import TileView from '../../../filmstrip/components/native/TileView';
  34. import { FILMSTRIP_SIZE } from '../../../filmstrip/constants';
  35. import { isFilmstripVisible } from '../../../filmstrip/functions.native';
  36. import CalleeInfoContainer from '../../../invite/components/callee-info/CalleeInfoContainer';
  37. import LargeVideo from '../../../large-video/components/LargeVideo.native';
  38. import { startKnocking } from '../../../lobby/actions.any';
  39. import { getIsLobbyVisible } from '../../../lobby/functions';
  40. import { navigate }
  41. from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
  42. import { shouldEnableAutoKnock } from '../../../mobile/navigation/functions';
  43. import { screen } from '../../../mobile/navigation/routes';
  44. import { setPictureInPictureEnabled } from '../../../mobile/picture-in-picture/functions';
  45. import Captions from '../../../subtitles/components/native/Captions';
  46. import { setToolboxVisible } from '../../../toolbox/actions';
  47. import Toolbox from '../../../toolbox/components/native/Toolbox';
  48. import { isToolboxVisible } from '../../../toolbox/functions';
  49. import {
  50. AbstractConference,
  51. abstractMapStateToProps
  52. } from '../AbstractConference';
  53. import type { AbstractProps } from '../AbstractConference';
  54. import { isConnecting } from '../functions';
  55. import AlwaysOnLabels from './AlwaysOnLabels';
  56. import ExpandedLabelPopup from './ExpandedLabelPopup';
  57. import LonelyMeetingExperience from './LonelyMeetingExperience';
  58. import TitleBar from './TitleBar';
  59. import { EXPANDED_LABEL_TIMEOUT } from './constants';
  60. import styles from './styles';
  61. /**
  62. * The type of the React {@code Component} props of {@link Conference}.
  63. */
  64. interface IProps extends AbstractProps {
  65. /**
  66. * Application's aspect ratio.
  67. */
  68. _aspectRatio: Symbol;
  69. /**
  70. * Whether the audio only is enabled or not.
  71. */
  72. _audioOnlyEnabled: boolean;
  73. /**
  74. * Branding styles for conference.
  75. */
  76. _brandingStyles: StyleType;
  77. /**
  78. * Whether the calendar feature is enabled or not.
  79. */
  80. _calendarEnabled: boolean;
  81. /**
  82. * The indicator which determines that we are still connecting to the
  83. * conference which includes establishing the XMPP connection and then
  84. * joining the room. If truthy, then an activity/loading indicator will be
  85. * rendered.
  86. */
  87. _connecting: boolean;
  88. /**
  89. * Set to {@code true} when the filmstrip is currently visible.
  90. */
  91. _filmstripVisible: boolean;
  92. /**
  93. * The indicator which determines whether fullscreen (immersive) mode is enabled.
  94. */
  95. _fullscreenEnabled: boolean;
  96. /**
  97. * The indicator which determines if the conference type is one to one.
  98. */
  99. _isOneToOneConference: boolean;
  100. /**
  101. * The indicator which determines if the participants pane is open.
  102. */
  103. _isParticipantsPaneOpen: boolean;
  104. /**
  105. * The ID of the participant currently on stage (if any).
  106. */
  107. _largeVideoParticipantId: string;
  108. /**
  109. * Local participant's display name.
  110. */
  111. _localParticipantDisplayName: string;
  112. /**
  113. * Whether Picture-in-Picture is enabled.
  114. */
  115. _pictureInPictureEnabled: boolean;
  116. /**
  117. * The indicator which determines whether the UI is reduced (to accommodate
  118. * smaller display areas).
  119. */
  120. _reducedUI: boolean;
  121. /**
  122. * Indicates if we should auto-knock.
  123. */
  124. _shouldEnableAutoKnock: boolean;
  125. /**
  126. * Indicates whether the lobby screen should be visible.
  127. */
  128. _showLobby: boolean;
  129. /**
  130. * Indicates whether the car mode is enabled.
  131. */
  132. _startCarMode: boolean;
  133. /**
  134. * The indicator which determines whether the Toolbox is visible.
  135. */
  136. _toolboxVisible: boolean;
  137. /**
  138. * The redux {@code dispatch} function.
  139. */
  140. dispatch: IStore['dispatch'];
  141. /**
  142. * Object containing the safe area insets.
  143. */
  144. insets: EdgeInsets;
  145. /**
  146. * Default prop for navigating between screen components(React Navigation).
  147. */
  148. navigation: any;
  149. }
  150. type State = {
  151. /**
  152. * The label that is currently expanded.
  153. */
  154. visibleExpandedLabel?: string;
  155. };
  156. /**
  157. * The conference page of the mobile (i.e. React Native) application.
  158. */
  159. class Conference extends AbstractConference<IProps, State> {
  160. /**
  161. * Timeout ref.
  162. */
  163. _expandedLabelTimeout: any;
  164. /**
  165. * Initializes a new Conference instance.
  166. *
  167. * @param {Object} props - The read-only properties with which the new
  168. * instance is to be initialized.
  169. */
  170. constructor(props: IProps) {
  171. super(props);
  172. this.state = {
  173. visibleExpandedLabel: undefined
  174. };
  175. this._expandedLabelTimeout = React.createRef<number>();
  176. // Bind event handlers so they are only bound once per instance.
  177. this._onClick = this._onClick.bind(this);
  178. this._onHardwareBackPress = this._onHardwareBackPress.bind(this);
  179. this._setToolboxVisible = this._setToolboxVisible.bind(this);
  180. this._createOnPress = this._createOnPress.bind(this);
  181. }
  182. /**
  183. * Implements {@link Component#componentDidMount()}. Invoked immediately
  184. * after this component is mounted.
  185. *
  186. * @inheritdoc
  187. * @returns {void}
  188. */
  189. componentDidMount() {
  190. const {
  191. _audioOnlyEnabled,
  192. _startCarMode,
  193. navigation
  194. } = this.props;
  195. BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
  196. if (_audioOnlyEnabled && _startCarMode) {
  197. navigation.navigate(screen.conference.carmode);
  198. }
  199. }
  200. /**
  201. * Implements {@code Component#componentDidUpdate}.
  202. *
  203. * @inheritdoc
  204. */
  205. componentDidUpdate(prevProps: IProps) {
  206. const {
  207. _shouldEnableAutoKnock,
  208. _showLobby,
  209. dispatch
  210. } = this.props;
  211. if (!prevProps._showLobby && _showLobby) {
  212. navigate(screen.lobby.root);
  213. if (_shouldEnableAutoKnock) {
  214. dispatch(startKnocking());
  215. }
  216. }
  217. if (prevProps._showLobby && !_showLobby) {
  218. navigate(screen.conference.main);
  219. }
  220. }
  221. /**
  222. * Implements {@link Component#componentWillUnmount()}. Invoked immediately
  223. * before this component is unmounted and destroyed. Disconnects the
  224. * conference described by the redux store/state.
  225. *
  226. * @inheritdoc
  227. * @returns {void}
  228. */
  229. componentWillUnmount() {
  230. // Tear handling any hardware button presses for back navigation down.
  231. BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
  232. clearTimeout(this._expandedLabelTimeout.current ?? 0);
  233. }
  234. /**
  235. * Implements React's {@link Component#render()}.
  236. *
  237. * @inheritdoc
  238. * @returns {ReactElement}
  239. */
  240. render() {
  241. const {
  242. _brandingStyles,
  243. _fullscreenEnabled
  244. } = this.props;
  245. return (
  246. <Container
  247. style = { [
  248. styles.conference,
  249. _brandingStyles
  250. ] }>
  251. <BrandingImageBackground />
  252. {
  253. Platform.OS === 'android'
  254. && <StatusBar
  255. barStyle = 'light-content'
  256. hidden = { _fullscreenEnabled }
  257. translucent = { _fullscreenEnabled } />
  258. }
  259. { this._renderContent() }
  260. </Container>
  261. );
  262. }
  263. /**
  264. * Changes the value of the toolboxVisible state, thus allowing us to switch
  265. * between Toolbox and Filmstrip and change their visibility.
  266. *
  267. * @private
  268. * @returns {void}
  269. */
  270. _onClick() {
  271. this._setToolboxVisible(!this.props._toolboxVisible);
  272. }
  273. /**
  274. * Handles a hardware button press for back navigation. Enters Picture-in-Picture mode
  275. * (if supported) or leaves the associated {@code Conference} otherwise.
  276. *
  277. * @returns {boolean} Exiting the app is undesired, so {@code true} is always returned.
  278. */
  279. _onHardwareBackPress() {
  280. let p;
  281. if (this.props._pictureInPictureEnabled) {
  282. const { PictureInPicture } = NativeModules;
  283. p = PictureInPicture.enterPictureInPicture();
  284. } else {
  285. p = Promise.reject(new Error('PiP not enabled'));
  286. }
  287. p.catch(() => {
  288. this.props.dispatch(appNavigate(undefined));
  289. });
  290. return true;
  291. }
  292. /**
  293. * Creates a function to be invoked when the onPress of the touchables are
  294. * triggered.
  295. *
  296. * @param {string} label - The identifier of the label that's onLayout is
  297. * triggered.
  298. * @returns {Function}
  299. */
  300. _createOnPress(label: string) {
  301. return () => {
  302. const { visibleExpandedLabel } = this.state;
  303. const newVisibleExpandedLabel
  304. = visibleExpandedLabel === label ? undefined : label;
  305. clearTimeout(this._expandedLabelTimeout.current);
  306. this.setState({
  307. visibleExpandedLabel: newVisibleExpandedLabel
  308. });
  309. if (newVisibleExpandedLabel) {
  310. this._expandedLabelTimeout.current = setTimeout(() => {
  311. this.setState({
  312. visibleExpandedLabel: undefined
  313. });
  314. }, EXPANDED_LABEL_TIMEOUT);
  315. }
  316. };
  317. }
  318. /**
  319. * Renders the content for the Conference container.
  320. *
  321. * @private
  322. * @returns {React$Element}
  323. */
  324. _renderContent() {
  325. const {
  326. _aspectRatio,
  327. _connecting,
  328. _filmstripVisible,
  329. _isOneToOneConference,
  330. _largeVideoParticipantId,
  331. _reducedUI,
  332. _shouldDisplayTileView,
  333. _toolboxVisible
  334. } = this.props;
  335. let alwaysOnTitleBarStyles;
  336. if (_reducedUI) {
  337. return this._renderContentForReducedUi();
  338. }
  339. if (_aspectRatio === ASPECT_RATIO_WIDE) {
  340. alwaysOnTitleBarStyles
  341. = !_shouldDisplayTileView && _filmstripVisible
  342. ? styles.alwaysOnTitleBarWide
  343. : styles.alwaysOnTitleBar;
  344. } else {
  345. alwaysOnTitleBarStyles = styles.alwaysOnTitleBar;
  346. }
  347. return (
  348. <>
  349. {/*
  350. * The LargeVideo is the lowermost stacking layer.
  351. */
  352. _shouldDisplayTileView
  353. ? <TileView onClick = { this._onClick } />
  354. : <LargeVideo onClick = { this._onClick } />
  355. }
  356. {/*
  357. * If there is a ringing call, show the callee's info.
  358. */
  359. <CalleeInfoContainer />
  360. }
  361. {/*
  362. * The activity/loading indicator goes above everything, except
  363. * the toolbox/toolbars and the dialogs.
  364. */
  365. _connecting
  366. && <TintedView>
  367. <LoadingIndicator />
  368. </TintedView>
  369. }
  370. <View
  371. pointerEvents = 'box-none'
  372. style = { styles.toolboxAndFilmstripContainer as ViewStyle }>
  373. <Captions onPress = { this._onClick } />
  374. {
  375. _shouldDisplayTileView || (
  376. !_isOneToOneConference
  377. && <Container style = { styles.displayNameContainer }>
  378. <DisplayNameLabel
  379. participantId = { _largeVideoParticipantId } />
  380. </Container>
  381. )
  382. }
  383. { !_shouldDisplayTileView && <LonelyMeetingExperience /> }
  384. {
  385. _shouldDisplayTileView
  386. || <>
  387. <Filmstrip />
  388. { this._renderNotificationsContainer() }
  389. <Toolbox />
  390. </>
  391. }
  392. </View>
  393. <SafeAreaView
  394. pointerEvents = 'box-none'
  395. style = {
  396. (_toolboxVisible
  397. ? styles.titleBarSafeViewColor
  398. : styles.titleBarSafeViewTransparent) as ViewStyle }>
  399. <TitleBar _createOnPress = { this._createOnPress } />
  400. </SafeAreaView>
  401. <SafeAreaView
  402. pointerEvents = 'box-none'
  403. style = {
  404. (_toolboxVisible
  405. ? [ styles.titleBarSafeViewTransparent, { top: this.props.insets.top + 50 } ]
  406. : styles.titleBarSafeViewTransparent) as ViewStyle
  407. }>
  408. <View
  409. pointerEvents = 'box-none'
  410. style = { styles.expandedLabelWrapper }>
  411. <ExpandedLabelPopup visibleExpandedLabel = { this.state.visibleExpandedLabel } />
  412. </View>
  413. <View
  414. pointerEvents = 'box-none'
  415. style = { alwaysOnTitleBarStyles as ViewStyle }>
  416. {/* eslint-disable-next-line react/jsx-no-bind */}
  417. <AlwaysOnLabels createOnPress = { this._createOnPress } />
  418. </View>
  419. </SafeAreaView>
  420. <TestConnectionInfo />
  421. {
  422. _shouldDisplayTileView
  423. && <>
  424. { this._renderNotificationsContainer() }
  425. <Toolbox />
  426. </>
  427. }
  428. </>
  429. );
  430. }
  431. /**
  432. * Renders the content for the Conference container when in "reduced UI" mode.
  433. *
  434. * @private
  435. * @returns {React$Element}
  436. */
  437. _renderContentForReducedUi() {
  438. const { _connecting } = this.props;
  439. return (
  440. <>
  441. <LargeVideo onClick = { this._onClick } />
  442. {
  443. _connecting
  444. && <TintedView>
  445. <LoadingIndicator />
  446. </TintedView>
  447. }
  448. </>
  449. );
  450. }
  451. /**
  452. * Renders a container for notifications to be displayed by the
  453. * base/notifications feature.
  454. *
  455. * @private
  456. * @returns {React$Element}
  457. */
  458. _renderNotificationsContainer() {
  459. const notificationsStyle: ViewStyle = {};
  460. // In the landscape mode (wide) there's problem with notifications being
  461. // shadowed by the filmstrip rendered on the right. This makes the "x"
  462. // button not clickable. In order to avoid that a margin of the
  463. // filmstrip's size is added to the right.
  464. //
  465. // Pawel: after many attempts I failed to make notifications adjust to
  466. // their contents width because of column and rows being used in the
  467. // flex layout. The only option that seemed to limit the notification's
  468. // size was explicit 'width' value which is not better than the margin
  469. // added here.
  470. const { _aspectRatio, _filmstripVisible } = this.props;
  471. if (_filmstripVisible && _aspectRatio !== ASPECT_RATIO_NARROW) {
  472. notificationsStyle.marginRight = FILMSTRIP_SIZE;
  473. }
  474. return super.renderNotificationsContainer(
  475. {
  476. shouldDisplayTileView: this.props._shouldDisplayTileView,
  477. style: notificationsStyle,
  478. toolboxVisible: this.props._toolboxVisible
  479. }
  480. );
  481. }
  482. /**
  483. * Dispatches an action changing the visibility of the {@link Toolbox}.
  484. *
  485. * @private
  486. * @param {boolean} visible - Pass {@code true} to show the
  487. * {@code Toolbox} or {@code false} to hide it.
  488. * @returns {void}
  489. */
  490. _setToolboxVisible(visible: boolean) {
  491. this.props.dispatch(setToolboxVisible(visible));
  492. }
  493. }
  494. /**
  495. * Maps (parts of) the redux state to the associated {@code Conference}'s props.
  496. *
  497. * @param {Object} state - The redux state.
  498. * @param {any} _ownProps - Component's own props.
  499. * @private
  500. * @returns {IProps}
  501. */
  502. function _mapStateToProps(state: IReduxState, _ownProps: any) {
  503. const { isOpen } = state['features/participants-pane'];
  504. const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
  505. const { backgroundColor } = state['features/dynamic-branding'];
  506. const { startCarMode } = state['features/base/settings'];
  507. const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
  508. const participantCount = getParticipantCount(state);
  509. const brandingStyles = backgroundColor ? {
  510. backgroundColor
  511. } : undefined;
  512. return {
  513. ...abstractMapStateToProps(state),
  514. _aspectRatio: aspectRatio,
  515. _audioOnlyEnabled: Boolean(audioOnlyEnabled),
  516. _brandingStyles: brandingStyles,
  517. _calendarEnabled: isCalendarEnabled(state),
  518. _connecting: isConnecting(state),
  519. _filmstripVisible: isFilmstripVisible(state),
  520. _fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
  521. _isOneToOneConference: Boolean(participantCount === 2),
  522. _isParticipantsPaneOpen: isOpen,
  523. _largeVideoParticipantId: state['features/large-video'].participantId,
  524. _pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
  525. _reducedUI: reducedUI,
  526. _shouldEnableAutoKnock: shouldEnableAutoKnock(state),
  527. _showLobby: getIsLobbyVisible(state),
  528. _startCarMode: startCarMode,
  529. _toolboxVisible: isToolboxVisible(state)
  530. };
  531. }
  532. export default withSafeAreaInsets(connect(_mapStateToProps)(props => {
  533. const dispatch = useDispatch();
  534. useFocusEffect(useCallback(() => {
  535. dispatch({ type: CONFERENCE_FOCUSED });
  536. setPictureInPictureEnabled(true);
  537. return () => {
  538. dispatch({ type: CONFERENCE_BLURRED });
  539. setPictureInPictureEnabled(false);
  540. };
  541. }, []));
  542. return ( // @ts-ignore
  543. <Conference { ...props } />
  544. );
  545. }));