// @flow import React from 'react'; import { NativeModules, SafeAreaView, StatusBar, View } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import { appNavigate } from '../../../app'; import { PIP_ENABLED, getFeatureFlag } from '../../../base/flags'; import { getParticipantCount } from '../../../base/participants'; import { Container, LoadingIndicator, TintedView } from '../../../base/react'; import { connect } from '../../../base/redux'; import { isNarrowAspectRatio, makeAspectRatioAware } from '../../../base/responsive-ui'; import { TestConnectionInfo } from '../../../base/testing'; import { ConferenceNotification, isCalendarEnabled } from '../../../calendar-sync'; import { Chat } from '../../../chat'; import { DisplayNameLabel } from '../../../display-name'; import { FILMSTRIP_SIZE, Filmstrip, isFilmstripVisible, TileView } from '../../../filmstrip'; import { LargeVideo } from '../../../large-video'; import { BackButtonRegistry } from '../../../mobile/back-button'; import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite'; import { Captions } from '../../../subtitles'; import { setToolboxVisible, Toolbox } from '../../../toolbox'; import { AbstractConference, abstractMapStateToProps } from '../AbstractConference'; import Labels from './Labels'; import NavigationBar from './NavigationBar'; import styles, { NAVBAR_GRADIENT_COLORS } from './styles'; import type { AbstractProps } from '../AbstractConference'; /** * The type of the React {@code Component} props of {@link Conference}. */ type Props = AbstractProps & { /** * Wherther the calendar feature is enabled or not. * * @private */ _calendarEnabled: boolean, /** * The indicator which determines that we are still connecting to the * conference which includes establishing the XMPP connection and then * joining the room. If truthy, then an activity/loading indicator will be * rendered. * * @private */ _connecting: boolean, /** * Set to {@code true} when the filmstrip is currently visible. * * @private */ _filmstripVisible: boolean, /** * The ID of the participant currently on stage (if any) */ _largeVideoParticipantId: string, /** * The number of participants in the conference. * * @private */ _participantCount: number, /** * Whether Picture-in-Picture is enabled. * * @private */ _pictureInPictureEnabled: boolean, /** * The indicator which determines whether the UI is reduced (to accommodate * smaller display areas). * * @private */ _reducedUI: boolean, /** * The handler which dispatches the (redux) action {@link setToolboxVisible} * to show/hide the {@link Toolbox}. * * @param {boolean} visible - {@code true} to show the {@code Toolbox} or * {@code false} to hide it. * @private * @returns {void} */ _setToolboxVisible: Function, /** * The indicator which determines whether the Toolbox is visible. * * @private */ _toolboxVisible: boolean, /** * The redux {@code dispatch} function. */ dispatch: Function }; /** * The conference page of the mobile (i.e. React Native) application. */ class Conference extends AbstractConference { /** * Initializes a new Conference instance. * * @param {Object} props - The read-only properties with which the new * instance is to be initialized. */ constructor(props) { super(props); // Bind event handlers so they are only bound once per instance. this._onClick = this._onClick.bind(this); this._onHardwareBackPress = this._onHardwareBackPress.bind(this); this._setToolboxVisible = this._setToolboxVisible.bind(this); } /** * Implements {@link Component#componentDidMount()}. Invoked immediately * after this component is mounted. * * @inheritdoc * @returns {void} */ componentDidMount() { BackButtonRegistry.addListener(this._onHardwareBackPress); // Show the toolbox if we are the only participant; otherwise, the whole // UI looks too unpopulated the LargeVideo visible. this.props._participantCount === 1 && this._setToolboxVisible(true); } /** * Implements React's {@link Component#componentDidUpdate()}. * * @inheritdoc */ componentDidUpdate(prevProps: Props) { const { _participantCount: oldParticipantCount } = prevProps; const { _participantCount: newParticipantCount, _toolboxVisible } = this.props; if (oldParticipantCount === 1 && newParticipantCount > 1 && _toolboxVisible) { this._setToolboxVisible(false); } else if (oldParticipantCount > 1 && newParticipantCount === 1 && !_toolboxVisible) { this._setToolboxVisible(true); } } /** * Implements {@link Component#componentWillUnmount()}. Invoked immediately * before this component is unmounted and destroyed. Disconnects the * conference described by the redux store/state. * * @inheritdoc * @returns {void} */ componentWillUnmount() { // Tear handling any hardware button presses for back navigation down. BackButtonRegistry.removeListener(this._onHardwareBackPress); } /** * Implements React's {@link Component#render()}. * * @inheritdoc * @returns {ReactElement} */ render() { const { _connecting, _filmstripVisible, _largeVideoParticipantId, _reducedUI, _shouldDisplayTileView, _toolboxVisible } = this.props; const showGradient = _toolboxVisible; const applyGradientStretching = _filmstripVisible && isNarrowAspectRatio(this) && !_shouldDisplayTileView; return ( ); } _onClick: () => void; /** * Changes the value of the toolboxVisible state, thus allowing us to switch * between Toolbox and Filmstrip and change their visibility. * * @private * @returns {void} */ _onClick() { this._setToolboxVisible(!this.props._toolboxVisible); } _onHardwareBackPress: () => boolean; /** * Handles a hardware button press for back navigation. Enters Picture-in-Picture mode * (if supported) or leaves the associated {@code Conference} otherwise. * * @returns {boolean} Exiting the app is undesired, so {@code true} is always returned. */ _onHardwareBackPress() { let p; if (this.props._pictureInPictureEnabled) { const { PictureInPicture } = NativeModules; p = PictureInPicture.enterPictureInPicture(); } else { p = Promise.reject(new Error('PiP not enabled')); } p.catch(() => { this.props.dispatch(appNavigate(undefined)); }); return true; } /** * Renders the conference notification badge if the feature is enabled. * * @private * @returns {React$Node} */ _renderConferenceNotification() { const { _calendarEnabled, _reducedUI } = this.props; return ( _calendarEnabled && !_reducedUI ? : undefined); } /** * Renders a container for notifications to be displayed by the * base/notifications feature. * * @private * @returns {React$Element} */ renderNotificationsContainer() { const notificationsStyle = {}; // In the landscape mode (wide) there's problem with notifications being // shadowed by the filmstrip rendered on the right. This makes the "x" // button not clickable. In order to avoid that a margin of the // filmstrip's size is added to the right. // // Pawel: after many attempts I failed to make notifications adjust to // their contents width because of column and rows being used in the // flex layout. The only option that seemed to limit the notification's // size was explicit 'width' value which is not better than the margin // added here. if (this.props._filmstripVisible && !isNarrowAspectRatio(this)) { notificationsStyle.marginRight = FILMSTRIP_SIZE; } return super.renderNotificationsContainer( { style: notificationsStyle } ); } _setToolboxVisible: (boolean) => void; /** * Dispatches an action changing the visibility of the {@link Toolbox}. * * @private * @param {boolean} visible - Pass {@code true} to show the * {@code Toolbox} or {@code false} to hide it. * @returns {void} */ _setToolboxVisible(visible) { this.props.dispatch(setToolboxVisible(visible)); } } /** * Maps (parts of) the redux state to the associated {@code Conference}'s props. * * @param {Object} state - The redux state. * @private * @returns {Props} */ function _mapStateToProps(state) { const { connecting, connection } = state['features/base/connection']; const { conference, joining, leaving } = state['features/base/conference']; const { reducedUI } = state['features/base/responsive-ui']; const { visible } = state['features/toolbox']; // XXX There is a window of time between the successful establishment of the // XMPP connection and the subsequent commencement of joining the MUC during // which the app does not appear to be doing anything according to the redux // state. In order to not toggle the _connecting props during the window of // time in question, define _connecting as follows: // - the XMPP connection is connecting, or // - the XMPP connection is connected and the conference is joining, or // - the XMPP connection is connected and we have no conference yet, nor we // are leaving one. const connecting_ = connecting || (connection && (joining || (!conference && !leaving))); return { ...abstractMapStateToProps(state), /** * Wherther the calendar feature is enabled or not. * * @private * @type {boolean} */ _calendarEnabled: isCalendarEnabled(state), /** * The indicator which determines that we are still connecting to the * conference which includes establishing the XMPP connection and then * joining the room. If truthy, then an activity/loading indicator will * be rendered. * * @private * @type {boolean} */ _connecting: Boolean(connecting_), /** * Is {@code true} when the filmstrip is currently visible. */ _filmstripVisible: isFilmstripVisible(state), /** * The ID of the participant currently on stage. */ _largeVideoParticipantId: state['features/large-video'].participantId, /** * The number of participants in the conference. * * @private * @type {number} */ _participantCount: getParticipantCount(state), /** * Whether Picture-in-Picture is enabled. * * @private * @type {boolean} */ _pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED), /** * The indicator which determines whether the UI is reduced (to * accommodate smaller display areas). * * @private * @type {boolean} */ _reducedUI: reducedUI, /** * The indicator which determines whether the Toolbox is visible. * * @private * @type {boolean} */ _toolboxVisible: visible }; } export default connect(_mapStateToProps)(makeAspectRatioAware(Conference));