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 '../../google-api';
import '../../mobile/audio-mode';
import '../../mobile/back-button';
import '../../mobile/background';
import '../../mobile/call-integration';
import '../../mobile/external-api';

View File

@ -8,6 +8,8 @@ import {
View
} from 'react-native';
import { BackButtonRegistry } from '../../../../mobile/back-button';
import { type StyleType } from '../../../styles';
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.
this._onHardwareBackPress = this._onHardwareBackPress.bind(this);
this._onHide = this._onHide.bind(this);
}
@ -119,6 +122,8 @@ export default class SlidingView extends PureComponent<Props, State> {
* @inheritdoc
*/
componentDidMount() {
BackButtonRegistry.addListener(this._onHardwareBackPress, true);
this._mounted = true;
this._setShow(this.props.show);
}
@ -142,6 +147,8 @@ export default class SlidingView extends PureComponent<Props, State> {
* @inheritdoc
*/
componentWillUnmount() {
BackButtonRegistry.removeListener(this._onHardwareBackPress);
this._mounted = false;
}
@ -215,6 +222,23 @@ export default class SlidingView extends PureComponent<Props, State> {
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;
/**

View File

@ -23,6 +23,17 @@ import styles from './styles';
* the mobile client.
*/
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()}.
*
@ -31,6 +42,7 @@ class Chat extends AbstractChat<Props> {
render() {
return (
<SlidingView
onHide = { this._onClose }
position = 'bottom'
show = { this.props._isOpen } >
<KeyboardAvoidingView
@ -38,7 +50,7 @@ class Chat extends AbstractChat<Props> {
style = { styles.chatContainer }>
<HeaderWithNavigation
headerLabelKey = 'chat.title'
onPressBack = { this.props._onToggleChat } />
onPressBack = { this._onClose } />
<SafeAreaView style = { styles.backdrop }>
<MessageContainer messages = { this.props._messages } />
<ChatInputBar onSend = { this.props._onSendMessage } />
@ -47,6 +59,23 @@ class Chat extends AbstractChat<Props> {
</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));

View File

@ -1,7 +1,7 @@
// @flow
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 { PIP_ENABLED, getFeatureFlag } from '../../../base/flags';
@ -23,6 +23,7 @@ import {
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';
@ -144,7 +145,7 @@ class Conference extends AbstractConference<Props, *> {
* @returns {void}
*/
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
BackButtonRegistry.addListener(this._onHardwareBackPress);
// Show the toolbox if we are the only participant; otherwise, the whole
// UI looks too unpopulated the LargeVideo visible.
@ -186,7 +187,7 @@ class Conference extends AbstractConference<Props, *> {
*/
componentWillUnmount() {
// 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 (
<SlidingView
onHide = { this._onCloseAddPeopleDialog }
position = 'bottom'
show = { this.props._isVisible } >
<HeaderWithNavigation
@ -242,15 +243,21 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
this._onTypeQuery('');
}
_onCloseAddPeopleDialog: () => void
_onCloseAddPeopleDialog: () => boolean
/**
* Closes the dialog.
*
* @returns {void}
* @returns {boolean}
*/
_onCloseAddPeopleDialog() {
if (this.props._isVisible) {
this.props.dispatch(setAddPeopleDialogVisible(false));
return true;
}
return false;
}
_onInvite: () => void

View File

@ -59,6 +59,7 @@ class DialInSummary extends Component<Props> {
return (
<SlidingView
onHide = { this._onCloseView }
position = 'bottom'
show = { Boolean(_summaryUrl) } >
<View style = { styles.webViewWrapper }>
@ -77,15 +78,21 @@ class DialInSummary extends Component<Props> {
);
}
_onCloseView: () => void;
_onCloseView: () => boolean;
/**
* Closes the view.
*
* @returns {void}
* @returns {boolean}
*/
_onCloseView() {
if (this.props._summaryUrl) {
this.props.dispatch(hideDialInSummary());
return true;
}
return false;
}
_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 { ColorSchemeRegistry } from '../../../base/color-scheme';
import {
BottomSheet
} from '../../../base/dialog';
import { BottomSheet, isDialogOpen } from '../../../base/dialog';
import { getParticipantDisplayName } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
@ -41,12 +39,20 @@ type Props = {
*/
_bottomSheetStyles: StyleType,
/**
* True if the menu is currently open, false otherwise.
*/
_isOpen: boolean,
/**
* Display name of the participant retreived from Redux.
*/
_participantDisplayName: string
}
// eslint-disable-next-line prefer-const
let RemoteVideoMenu_;
/**
* 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}.
*
* @private
* @returns {void}
* @returns {boolean}
*/
_onCancel() {
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} ownProps - Properties of component.
* @private
* @returns {{
* _bottomSheetStyles: StyleType,
* _participantDisplayName: string
* }}
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { participant } = ownProps;
@ -123,9 +132,12 @@ function _mapStateToProps(state, ownProps) {
return {
_bottomSheetStyles:
ColorSchemeRegistry.get(state, 'BottomSheet'),
_isOpen: isDialogOpen(state, RemoteVideoMenu_),
_participantDisplayName: getParticipantDisplayName(
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 { 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 { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
@ -34,6 +34,11 @@ type Props = {
*/
_chatEnabled: boolean,
/**
* True if the overflow menu is currently visible, false otherwise.
*/
_isOpen: boolean,
/**
* 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}.
*
* @private
* @returns {void}
* @returns {boolean}
*/
_onCancel() {
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.
* @private
* @returns {{
* _bottomSheetStyles: StyleType,
* _chatEnabled: boolean,
* _recordingEnabled: boolean
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
_bottomSheetStyles:
ColorSchemeRegistry.get(state, 'BottomSheet'),
_chatEnabled: getFeatureFlag(state, CHAT_ENABLED, true),
_isOpen: isDialogOpen(state, OverflowMenu_),
_recordingEnabled: Platform.OS !== 'ios' || getFeatureFlag(state, IOS_RECORDING_ENABLED)
};
}