feat: central back button registry

This commit is contained in:
Bettenbuk Zoltan 2019-07-11 13:32:17 +02:00 committed by Zoltan Bettenbuk
parent b86df7a8e3
commit 0a76eebca7
11 changed files with 226 additions and 30 deletions

View File

@ -16,6 +16,7 @@ import {
import { updateSettings } from '../../base/settings'; import { updateSettings } from '../../base/settings';
import '../../google-api'; import '../../google-api';
import '../../mobile/audio-mode'; import '../../mobile/audio-mode';
import '../../mobile/back-button';
import '../../mobile/background'; import '../../mobile/background';
import '../../mobile/call-integration'; import '../../mobile/call-integration';
import '../../mobile/external-api'; import '../../mobile/external-api';

View File

@ -8,6 +8,8 @@ import {
View View
} from 'react-native'; } from 'react-native';
import { BackButtonRegistry } from '../../../../mobile/back-button';
import { type StyleType } from '../../../styles'; import { type StyleType } from '../../../styles';
import styles from './slidingviewstyles'; import styles from './slidingviewstyles';
@ -110,6 +112,7 @@ export default class SlidingView extends PureComponent<Props, State> {
}; };
// Bind event handlers so they are only bound once per instance. // Bind event handlers so they are only bound once per instance.
this._onHardwareBackPress = this._onHardwareBackPress.bind(this);
this._onHide = this._onHide.bind(this); this._onHide = this._onHide.bind(this);
} }
@ -119,6 +122,8 @@ export default class SlidingView extends PureComponent<Props, State> {
* @inheritdoc * @inheritdoc
*/ */
componentDidMount() { componentDidMount() {
BackButtonRegistry.addListener(this._onHardwareBackPress, true);
this._mounted = true; this._mounted = true;
this._setShow(this.props.show); this._setShow(this.props.show);
} }
@ -142,6 +147,8 @@ export default class SlidingView extends PureComponent<Props, State> {
* @inheritdoc * @inheritdoc
*/ */
componentWillUnmount() { componentWillUnmount() {
BackButtonRegistry.removeListener(this._onHardwareBackPress);
this._mounted = false; this._mounted = false;
} }
@ -215,6 +222,23 @@ export default class SlidingView extends PureComponent<Props, State> {
return style; return style;
} }
_onHardwareBackPress: () => boolean;
/**
* Callback to handle the hardware back button.
*
* @returns {boolean}
*/
_onHardwareBackPress() {
const { onHide } = this.props;
if (typeof onHide === 'function') {
return onHide();
}
return false;
}
_onHide: () => void; _onHide: () => void;
/** /**

View File

@ -23,6 +23,17 @@ import styles from './styles';
* the mobile client. * the mobile client.
*/ */
class Chat extends AbstractChat<Props> { class Chat extends AbstractChat<Props> {
/**
* Instantiates a new instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onClose = this._onClose.bind(this);
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -31,6 +42,7 @@ class Chat extends AbstractChat<Props> {
render() { render() {
return ( return (
<SlidingView <SlidingView
onHide = { this._onClose }
position = 'bottom' position = 'bottom'
show = { this.props._isOpen } > show = { this.props._isOpen } >
<KeyboardAvoidingView <KeyboardAvoidingView
@ -38,7 +50,7 @@ class Chat extends AbstractChat<Props> {
style = { styles.chatContainer }> style = { styles.chatContainer }>
<HeaderWithNavigation <HeaderWithNavigation
headerLabelKey = 'chat.title' headerLabelKey = 'chat.title'
onPressBack = { this.props._onToggleChat } /> onPressBack = { this._onClose } />
<SafeAreaView style = { styles.backdrop }> <SafeAreaView style = { styles.backdrop }>
<MessageContainer messages = { this.props._messages } /> <MessageContainer messages = { this.props._messages } />
<ChatInputBar onSend = { this.props._onSendMessage } /> <ChatInputBar onSend = { this.props._onSendMessage } />
@ -47,6 +59,23 @@ class Chat extends AbstractChat<Props> {
</SlidingView> </SlidingView>
); );
} }
_onClose: () => boolean
/**
* Closes the chat window.
*
* @returns {boolean}
*/
_onClose() {
if (this.props._isOpen) {
this.props._onToggleChat();
return true;
}
return false;
}
} }
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat)); export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));

View File

@ -1,7 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { BackHandler, NativeModules, SafeAreaView, StatusBar, View } from 'react-native'; import { NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
import { appNavigate } from '../../../app'; import { appNavigate } from '../../../app';
import { PIP_ENABLED, getFeatureFlag } from '../../../base/flags'; import { PIP_ENABLED, getFeatureFlag } from '../../../base/flags';
@ -23,6 +23,7 @@ import {
TileView TileView
} from '../../../filmstrip'; } from '../../../filmstrip';
import { LargeVideo } from '../../../large-video'; import { LargeVideo } from '../../../large-video';
import { BackButtonRegistry } from '../../../mobile/back-button';
import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite'; import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
import { Captions } from '../../../subtitles'; import { Captions } from '../../../subtitles';
import { setToolboxVisible, Toolbox } from '../../../toolbox'; import { setToolboxVisible, Toolbox } from '../../../toolbox';
@ -144,7 +145,7 @@ class Conference extends AbstractConference<Props, *> {
* @returns {void} * @returns {void}
*/ */
componentDidMount() { componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress); BackButtonRegistry.addListener(this._onHardwareBackPress);
// Show the toolbox if we are the only participant; otherwise, the whole // Show the toolbox if we are the only participant; otherwise, the whole
// UI looks too unpopulated the LargeVideo visible. // UI looks too unpopulated the LargeVideo visible.
@ -186,7 +187,7 @@ class Conference extends AbstractConference<Props, *> {
*/ */
componentWillUnmount() { componentWillUnmount() {
// Tear handling any hardware button presses for back navigation down. // Tear handling any hardware button presses for back navigation down.
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress); BackButtonRegistry.removeListener(this._onHardwareBackPress);
} }
/** /**

View File

@ -150,6 +150,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
return ( return (
<SlidingView <SlidingView
onHide = { this._onCloseAddPeopleDialog }
position = 'bottom' position = 'bottom'
show = { this.props._isVisible } > show = { this.props._isVisible } >
<HeaderWithNavigation <HeaderWithNavigation
@ -242,15 +243,21 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
this._onTypeQuery(''); this._onTypeQuery('');
} }
_onCloseAddPeopleDialog: () => void _onCloseAddPeopleDialog: () => boolean
/** /**
* Closes the dialog. * Closes the dialog.
* *
* @returns {void} * @returns {boolean}
*/ */
_onCloseAddPeopleDialog() { _onCloseAddPeopleDialog() {
this.props.dispatch(setAddPeopleDialogVisible(false)); if (this.props._isVisible) {
this.props.dispatch(setAddPeopleDialogVisible(false));
return true;
}
return false;
} }
_onInvite: () => void _onInvite: () => void

View File

@ -59,6 +59,7 @@ class DialInSummary extends Component<Props> {
return ( return (
<SlidingView <SlidingView
onHide = { this._onCloseView }
position = 'bottom' position = 'bottom'
show = { Boolean(_summaryUrl) } > show = { Boolean(_summaryUrl) } >
<View style = { styles.webViewWrapper }> <View style = { styles.webViewWrapper }>
@ -77,15 +78,21 @@ class DialInSummary extends Component<Props> {
); );
} }
_onCloseView: () => void; _onCloseView: () => boolean;
/** /**
* Closes the view. * Closes the view.
* *
* @returns {void} * @returns {boolean}
*/ */
_onCloseView() { _onCloseView() {
this.props.dispatch(hideDialInSummary()); if (this.props._summaryUrl) {
this.props.dispatch(hideDialInSummary());
return true;
}
return false;
} }
_onError: () => void; _onError: () => void;

View File

@ -0,0 +1,66 @@
// @flow
/**
* An registry that dispatches hardware back button events for subscribers with a custom logic.
*/
class BackButtonRegistry {
_listeners: Array<Function>;
/**
* Instantiates a new instance of the registry.
*/
constructor() {
this._listeners = [];
}
/**
* Adds a listener to the registry.
*
* NOTE: Due to the different order of component mounts, we allow a component to register
* its listener to the top of the list, so then that will be invoked before other, 'non-top'
* listeners. For example a 'non-top' listener can be the one that puts the app into PiP mode,
* while a 'top' listener is the one that closes a modal in a conference.
*
* @param {Function} listener - The listener function.
* @param {boolean?} top - If true, the listener will be put on the top (eg for modal-like components).
* @returns {void}
*/
addListener(listener: Function, top: boolean = false) {
if (top) {
this._listeners.splice(0, 0, listener);
} else {
this._listeners.push(listener);
}
}
/**
* Removes a listener from the registry.
*
* @param {Function} listener - The listener to remove.
* @returns {void}
*/
removeListener(listener: Function) {
this._listeners = this._listeners.filter(f => f !== listener);
}
onHardwareBackPress: () => boolean
/**
* Callback for the back button press event.
*
* @returns {boolean}
*/
onHardwareBackPress() {
for (const listener of this._listeners) {
const result = listener();
if (result === true) {
return true;
}
}
return false;
}
}
export default new BackButtonRegistry();

View File

@ -0,0 +1,5 @@
// @flow
export { default as BackButtonRegistry } from './BackButtonRegistry';
import './middleware';

View File

@ -0,0 +1,36 @@
// @flow
import { BackHandler } from 'react-native';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import { MiddlewareRegistry } from '../../base/redux';
import BackButtonRegistry from './BackButtonRegistry';
// Binding function to the proper instance, so then the event emitter won't replace the
// underlying instance.
BackButtonRegistry.onHardwareBackPress = BackButtonRegistry.onHardwareBackPress.bind(BackButtonRegistry);
/**
* Middleware that captures App lifetime actions and subscribes to application
* state changes. When the application state changes it will fire the action
* required to mute or unmute the local video in case the application goes to
* the background or comes back from it.
*
* @param {Store} store - The redux store.
* @returns {Function}
* @see {@link https://facebook.github.io/react-native/docs/appstate.html}
*/
MiddlewareRegistry.register(() => next => action => {
switch (action.type) {
case APP_WILL_MOUNT:
BackHandler.addEventListener('hardwareBackPress', BackButtonRegistry.onHardwareBackPress);
break;
case APP_WILL_UNMOUNT:
BackHandler.removeEventListener('hardwareBackPress', BackButtonRegistry.onHardwareBackPress);
break;
}
return next(action);
});

View File

@ -5,9 +5,7 @@ import { Text, View } from 'react-native';
import { Avatar } from '../../../base/avatar'; import { Avatar } from '../../../base/avatar';
import { ColorSchemeRegistry } from '../../../base/color-scheme'; import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { import { BottomSheet, isDialogOpen } from '../../../base/dialog';
BottomSheet
} from '../../../base/dialog';
import { getParticipantDisplayName } from '../../../base/participants'; import { getParticipantDisplayName } from '../../../base/participants';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles'; import { StyleType } from '../../../base/styles';
@ -41,12 +39,20 @@ type Props = {
*/ */
_bottomSheetStyles: StyleType, _bottomSheetStyles: StyleType,
/**
* True if the menu is currently open, false otherwise.
*/
_isOpen: boolean,
/** /**
* Display name of the participant retreived from Redux. * Display name of the participant retreived from Redux.
*/ */
_participantDisplayName: string _participantDisplayName: string
} }
// eslint-disable-next-line prefer-const
let RemoteVideoMenu_;
/** /**
* Class to implement a popup menu that opens upon long pressing a thumbnail. * Class to implement a popup menu that opens upon long pressing a thumbnail.
*/ */
@ -93,16 +99,22 @@ class RemoteVideoMenu extends Component<Props> {
); );
} }
_onCancel: () => void; _onCancel: () => boolean;
/** /**
* Callback to hide the {@code RemoteVideoMenu}. * Callback to hide the {@code RemoteVideoMenu}.
* *
* @private * @private
* @returns {void} * @returns {boolean}
*/ */
_onCancel() { _onCancel() {
this.props.dispatch(hideRemoteVideoMenu()); if (this.props._isOpen) {
this.props.dispatch(hideRemoteVideoMenu());
return true;
}
return false;
} }
} }
@ -112,10 +124,7 @@ class RemoteVideoMenu extends Component<Props> {
* @param {Object} state - Redux state. * @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component. * @param {Object} ownProps - Properties of component.
* @private * @private
* @returns {{ * @returns {Props}
* _bottomSheetStyles: StyleType,
* _participantDisplayName: string
* }}
*/ */
function _mapStateToProps(state, ownProps) { function _mapStateToProps(state, ownProps) {
const { participant } = ownProps; const { participant } = ownProps;
@ -123,9 +132,12 @@ function _mapStateToProps(state, ownProps) {
return { return {
_bottomSheetStyles: _bottomSheetStyles:
ColorSchemeRegistry.get(state, 'BottomSheet'), ColorSchemeRegistry.get(state, 'BottomSheet'),
_isOpen: isDialogOpen(state, RemoteVideoMenu_),
_participantDisplayName: getParticipantDisplayName( _participantDisplayName: getParticipantDisplayName(
state, participant.id) state, participant.id)
}; };
} }
export default connect(_mapStateToProps)(RemoteVideoMenu); RemoteVideoMenu_ = connect(_mapStateToProps)(RemoteVideoMenu);
export default RemoteVideoMenu_;

View File

@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme'; import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { BottomSheet, hideDialog } from '../../../base/dialog'; import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
import { CHAT_ENABLED, IOS_RECORDING_ENABLED, getFeatureFlag } from '../../../base/flags'; import { CHAT_ENABLED, IOS_RECORDING_ENABLED, getFeatureFlag } from '../../../base/flags';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles'; import { StyleType } from '../../../base/styles';
@ -34,6 +34,11 @@ type Props = {
*/ */
_chatEnabled: boolean, _chatEnabled: boolean,
/**
* True if the overflow menu is currently visible, false otherwise.
*/
_isOpen: boolean,
/** /**
* Whether the recoding button should be enabled or not. * Whether the recoding button should be enabled or not.
*/ */
@ -107,16 +112,22 @@ class OverflowMenu extends Component<Props> {
); );
} }
_onCancel: () => void; _onCancel: () => boolean;
/** /**
* Hides this {@code OverflowMenu}. * Hides this {@code OverflowMenu}.
* *
* @private * @private
* @returns {void} * @returns {boolean}
*/ */
_onCancel() { _onCancel() {
this.props.dispatch(hideDialog(OverflowMenu_)); if (this.props._isOpen) {
this.props.dispatch(hideDialog(OverflowMenu_));
return true;
}
return false;
} }
} }
@ -125,17 +136,14 @@ class OverflowMenu extends Component<Props> {
* *
* @param {Object} state - Redux state. * @param {Object} state - Redux state.
* @private * @private
* @returns {{ * @returns {Props}
* _bottomSheetStyles: StyleType,
* _chatEnabled: boolean,
* _recordingEnabled: boolean
* }}
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
return { return {
_bottomSheetStyles: _bottomSheetStyles:
ColorSchemeRegistry.get(state, 'BottomSheet'), ColorSchemeRegistry.get(state, 'BottomSheet'),
_chatEnabled: getFeatureFlag(state, CHAT_ENABLED, true), _chatEnabled: getFeatureFlag(state, CHAT_ENABLED, true),
_isOpen: isDialogOpen(state, OverflowMenu_),
_recordingEnabled: Platform.OS !== 'ios' || getFeatureFlag(state, IOS_RECORDING_ENABLED) _recordingEnabled: Platform.OS !== 'ios' || getFeatureFlag(state, IOS_RECORDING_ENABLED)
}; };
} }