feat: central back button registry
This commit is contained in:
parent
b86df7a8e3
commit
0a76eebca7
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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() {
|
||||
this.props.dispatch(setAddPeopleDialogVisible(false));
|
||||
if (this.props._isVisible) {
|
||||
this.props.dispatch(setAddPeopleDialogVisible(false));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_onInvite: () => void
|
||||
|
|
|
@ -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() {
|
||||
this.props.dispatch(hideDialInSummary());
|
||||
if (this.props._summaryUrl) {
|
||||
this.props.dispatch(hideDialInSummary());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_onError: () => void;
|
||||
|
|
|
@ -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();
|
|
@ -0,0 +1,5 @@
|
|||
// @flow
|
||||
|
||||
export { default as BackButtonRegistry } from './BackButtonRegistry';
|
||||
|
||||
import './middleware';
|
|
@ -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);
|
||||
});
|
|
@ -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() {
|
||||
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} 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_;
|
||||
|
|
|
@ -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() {
|
||||
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.
|
||||
* @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)
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue