Browse Source

feat: central back button registry

master
Bettenbuk Zoltan 5 years ago
parent
commit
0a76eebca7

+ 1
- 0
react/features/app/components/App.native.js View File

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

+ 24
- 0
react/features/base/react/components/native/SlidingView.js View File

@@ -8,6 +8,8 @@ import {
8 8
     View
9 9
 } from 'react-native';
10 10
 
11
+import { BackButtonRegistry } from '../../../../mobile/back-button';
12
+
11 13
 import { type StyleType } from '../../../styles';
12 14
 
13 15
 import styles from './slidingviewstyles';
@@ -110,6 +112,7 @@ export default class SlidingView extends PureComponent<Props, State> {
110 112
         };
111 113
 
112 114
         // Bind event handlers so they are only bound once per instance.
115
+        this._onHardwareBackPress = this._onHardwareBackPress.bind(this);
113 116
         this._onHide = this._onHide.bind(this);
114 117
     }
115 118
 
@@ -119,6 +122,8 @@ export default class SlidingView extends PureComponent<Props, State> {
119 122
      * @inheritdoc
120 123
      */
121 124
     componentDidMount() {
125
+        BackButtonRegistry.addListener(this._onHardwareBackPress, true);
126
+
122 127
         this._mounted = true;
123 128
         this._setShow(this.props.show);
124 129
     }
@@ -142,6 +147,8 @@ export default class SlidingView extends PureComponent<Props, State> {
142 147
      * @inheritdoc
143 148
      */
144 149
     componentWillUnmount() {
150
+        BackButtonRegistry.removeListener(this._onHardwareBackPress);
151
+
145 152
         this._mounted = false;
146 153
     }
147 154
 
@@ -215,6 +222,23 @@ export default class SlidingView extends PureComponent<Props, State> {
215 222
         return style;
216 223
     }
217 224
 
225
+    _onHardwareBackPress: () => boolean;
226
+
227
+    /**
228
+     * Callback to handle the hardware back button.
229
+     *
230
+     * @returns {boolean}
231
+     */
232
+    _onHardwareBackPress() {
233
+        const { onHide } = this.props;
234
+
235
+        if (typeof onHide === 'function') {
236
+            return onHide();
237
+        }
238
+
239
+        return false;
240
+    }
241
+
218 242
     _onHide: () => void;
219 243
 
220 244
     /**

+ 30
- 1
react/features/chat/components/native/Chat.js View File

@@ -23,6 +23,17 @@ import styles from './styles';
23 23
  * the mobile client.
24 24
  */
25 25
 class Chat extends AbstractChat<Props> {
26
+    /**
27
+     * Instantiates a new instance.
28
+     *
29
+     * @inheritdoc
30
+     */
31
+    constructor(props: Props) {
32
+        super(props);
33
+
34
+        this._onClose = this._onClose.bind(this);
35
+    }
36
+
26 37
     /**
27 38
      * Implements React's {@link Component#render()}.
28 39
      *
@@ -31,6 +42,7 @@ class Chat extends AbstractChat<Props> {
31 42
     render() {
32 43
         return (
33 44
             <SlidingView
45
+                onHide = { this._onClose }
34 46
                 position = 'bottom'
35 47
                 show = { this.props._isOpen } >
36 48
                 <KeyboardAvoidingView
@@ -38,7 +50,7 @@ class Chat extends AbstractChat<Props> {
38 50
                     style = { styles.chatContainer }>
39 51
                     <HeaderWithNavigation
40 52
                         headerLabelKey = 'chat.title'
41
-                        onPressBack = { this.props._onToggleChat } />
53
+                        onPressBack = { this._onClose } />
42 54
                     <SafeAreaView style = { styles.backdrop }>
43 55
                         <MessageContainer messages = { this.props._messages } />
44 56
                         <ChatInputBar onSend = { this.props._onSendMessage } />
@@ -47,6 +59,23 @@ class Chat extends AbstractChat<Props> {
47 59
             </SlidingView>
48 60
         );
49 61
     }
62
+
63
+    _onClose: () => boolean
64
+
65
+    /**
66
+     * Closes the chat window.
67
+     *
68
+     * @returns {boolean}
69
+     */
70
+    _onClose() {
71
+        if (this.props._isOpen) {
72
+            this.props._onToggleChat();
73
+
74
+            return true;
75
+        }
76
+
77
+        return false;
78
+    }
50 79
 }
51 80
 
52 81
 export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));

+ 4
- 3
react/features/conference/components/native/Conference.js View File

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

+ 10
- 3
react/features/invite/components/add-people-dialog/native/AddPeopleDialog.js View File

@@ -150,6 +150,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
150 150
 
151 151
         return (
152 152
             <SlidingView
153
+                onHide = { this._onCloseAddPeopleDialog }
153 154
                 position = 'bottom'
154 155
                 show = { this.props._isVisible } >
155 156
                 <HeaderWithNavigation
@@ -242,15 +243,21 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
242 243
         this._onTypeQuery('');
243 244
     }
244 245
 
245
-    _onCloseAddPeopleDialog: () => void
246
+    _onCloseAddPeopleDialog: () => boolean
246 247
 
247 248
     /**
248 249
      * Closes the dialog.
249 250
      *
250
-     * @returns {void}
251
+     * @returns {boolean}
251 252
      */
252 253
     _onCloseAddPeopleDialog() {
253
-        this.props.dispatch(setAddPeopleDialogVisible(false));
254
+        if (this.props._isVisible) {
255
+            this.props.dispatch(setAddPeopleDialogVisible(false));
256
+
257
+            return true;
258
+        }
259
+
260
+        return false;
254 261
     }
255 262
 
256 263
     _onInvite: () => void

+ 10
- 3
react/features/invite/components/dial-in-summary/native/DialInSummary.js View File

@@ -59,6 +59,7 @@ class DialInSummary extends Component<Props> {
59 59
 
60 60
         return (
61 61
             <SlidingView
62
+                onHide = { this._onCloseView }
62 63
                 position = 'bottom'
63 64
                 show = { Boolean(_summaryUrl) } >
64 65
                 <View style = { styles.webViewWrapper }>
@@ -77,15 +78,21 @@ class DialInSummary extends Component<Props> {
77 78
         );
78 79
     }
79 80
 
80
-    _onCloseView: () => void;
81
+    _onCloseView: () => boolean;
81 82
 
82 83
     /**
83 84
      * Closes the view.
84 85
      *
85
-     * @returns {void}
86
+     * @returns {boolean}
86 87
      */
87 88
     _onCloseView() {
88
-        this.props.dispatch(hideDialInSummary());
89
+        if (this.props._summaryUrl) {
90
+            this.props.dispatch(hideDialInSummary());
91
+
92
+            return true;
93
+        }
94
+
95
+        return false;
89 96
     }
90 97
 
91 98
     _onError: () => void;

+ 66
- 0
react/features/mobile/back-button/BackButtonRegistry.js View File

@@ -0,0 +1,66 @@
1
+// @flow
2
+
3
+/**
4
+ * An registry that dispatches hardware back button events for subscribers with a custom logic.
5
+ */
6
+class BackButtonRegistry {
7
+    _listeners: Array<Function>;
8
+
9
+    /**
10
+     * Instantiates a new instance of the registry.
11
+     */
12
+    constructor() {
13
+        this._listeners = [];
14
+    }
15
+
16
+    /**
17
+     * Adds a listener to the registry.
18
+     *
19
+     * NOTE: Due to the different order of component mounts, we allow a component to register
20
+     * its listener to the top of the list, so then that will be invoked before other, 'non-top'
21
+     * listeners. For example a 'non-top' listener can be the one that puts the app into PiP mode,
22
+     * while a 'top' listener is the one that closes a modal in a conference.
23
+     *
24
+     * @param {Function} listener - The listener function.
25
+     * @param {boolean?} top - If true, the listener will be put on the top (eg for modal-like components).
26
+     * @returns {void}
27
+     */
28
+    addListener(listener: Function, top: boolean = false) {
29
+        if (top) {
30
+            this._listeners.splice(0, 0, listener);
31
+        } else {
32
+            this._listeners.push(listener);
33
+        }
34
+    }
35
+
36
+    /**
37
+     * Removes a listener from the registry.
38
+     *
39
+     * @param {Function} listener - The listener to remove.
40
+     * @returns {void}
41
+     */
42
+    removeListener(listener: Function) {
43
+        this._listeners = this._listeners.filter(f => f !== listener);
44
+    }
45
+
46
+    onHardwareBackPress: () => boolean
47
+
48
+    /**
49
+     * Callback for the back button press event.
50
+     *
51
+     * @returns {boolean}
52
+     */
53
+    onHardwareBackPress() {
54
+        for (const listener of this._listeners) {
55
+            const result = listener();
56
+
57
+            if (result === true) {
58
+                return true;
59
+            }
60
+        }
61
+
62
+        return false;
63
+    }
64
+}
65
+
66
+export default new BackButtonRegistry();

+ 5
- 0
react/features/mobile/back-button/index.js View File

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

+ 36
- 0
react/features/mobile/back-button/middleware.js View File

@@ -0,0 +1,36 @@
1
+// @flow
2
+
3
+import { BackHandler } from 'react-native';
4
+
5
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
6
+import { MiddlewareRegistry } from '../../base/redux';
7
+
8
+import BackButtonRegistry from './BackButtonRegistry';
9
+
10
+// Binding function to the proper instance, so then the event emitter won't replace the
11
+// underlying instance.
12
+BackButtonRegistry.onHardwareBackPress = BackButtonRegistry.onHardwareBackPress.bind(BackButtonRegistry);
13
+
14
+/**
15
+ * Middleware that captures App lifetime actions and subscribes to application
16
+ * state changes. When the application state changes it will fire the action
17
+ * required to mute or unmute the local video in case the application goes to
18
+ * the background or comes back from it.
19
+ *
20
+ * @param {Store} store - The redux store.
21
+ * @returns {Function}
22
+ * @see {@link https://facebook.github.io/react-native/docs/appstate.html}
23
+ */
24
+MiddlewareRegistry.register(() => next => action => {
25
+    switch (action.type) {
26
+    case APP_WILL_MOUNT:
27
+        BackHandler.addEventListener('hardwareBackPress', BackButtonRegistry.onHardwareBackPress);
28
+        break;
29
+
30
+    case APP_WILL_UNMOUNT:
31
+        BackHandler.removeEventListener('hardwareBackPress', BackButtonRegistry.onHardwareBackPress);
32
+        break;
33
+    }
34
+
35
+    return next(action);
36
+});

+ 23
- 11
react/features/remote-video-menu/components/native/RemoteVideoMenu.js View File

@@ -5,9 +5,7 @@ import { Text, View } from 'react-native';
5 5
 
6 6
 import { Avatar } from '../../../base/avatar';
7 7
 import { ColorSchemeRegistry } from '../../../base/color-scheme';
8
-import {
9
-    BottomSheet
10
-} from '../../../base/dialog';
8
+import { BottomSheet, isDialogOpen } from '../../../base/dialog';
11 9
 import { getParticipantDisplayName } from '../../../base/participants';
12 10
 import { connect } from '../../../base/redux';
13 11
 import { StyleType } from '../../../base/styles';
@@ -41,12 +39,20 @@ type Props = {
41 39
      */
42 40
     _bottomSheetStyles: StyleType,
43 41
 
42
+    /**
43
+     * True if the menu is currently open, false otherwise.
44
+     */
45
+    _isOpen: boolean,
46
+
44 47
     /**
45 48
      * Display name of the participant retreived from Redux.
46 49
      */
47 50
     _participantDisplayName: string
48 51
 }
49 52
 
53
+// eslint-disable-next-line prefer-const
54
+let RemoteVideoMenu_;
55
+
50 56
 /**
51 57
  * Class to implement a popup menu that opens upon long pressing a thumbnail.
52 58
  */
@@ -93,16 +99,22 @@ class RemoteVideoMenu extends Component<Props> {
93 99
         );
94 100
     }
95 101
 
96
-    _onCancel: () => void;
102
+    _onCancel: () => boolean;
97 103
 
98 104
     /**
99 105
      * Callback to hide the {@code RemoteVideoMenu}.
100 106
      *
101 107
      * @private
102
-     * @returns {void}
108
+     * @returns {boolean}
103 109
      */
104 110
     _onCancel() {
105
-        this.props.dispatch(hideRemoteVideoMenu());
111
+        if (this.props._isOpen) {
112
+            this.props.dispatch(hideRemoteVideoMenu());
113
+
114
+            return true;
115
+        }
116
+
117
+        return false;
106 118
     }
107 119
 }
108 120
 
@@ -112,10 +124,7 @@ class RemoteVideoMenu extends Component<Props> {
112 124
  * @param {Object} state - Redux state.
113 125
  * @param {Object} ownProps - Properties of component.
114 126
  * @private
115
- * @returns {{
116
- *      _bottomSheetStyles: StyleType,
117
- *      _participantDisplayName: string
118
- *  }}
127
+ * @returns {Props}
119 128
  */
120 129
 function _mapStateToProps(state, ownProps) {
121 130
     const { participant } = ownProps;
@@ -123,9 +132,12 @@ function _mapStateToProps(state, ownProps) {
123 132
     return {
124 133
         _bottomSheetStyles:
125 134
             ColorSchemeRegistry.get(state, 'BottomSheet'),
135
+        _isOpen: isDialogOpen(state, RemoteVideoMenu_),
126 136
         _participantDisplayName: getParticipantDisplayName(
127 137
             state, participant.id)
128 138
     };
129 139
 }
130 140
 
131
-export default connect(_mapStateToProps)(RemoteVideoMenu);
141
+RemoteVideoMenu_ = connect(_mapStateToProps)(RemoteVideoMenu);
142
+
143
+export default RemoteVideoMenu_;

+ 17
- 9
react/features/toolbox/components/native/OverflowMenu.js View File

@@ -4,7 +4,7 @@ import React, { Component } from 'react';
4 4
 import { Platform } from 'react-native';
5 5
 
6 6
 import { ColorSchemeRegistry } from '../../../base/color-scheme';
7
-import { BottomSheet, hideDialog } from '../../../base/dialog';
7
+import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
8 8
 import { CHAT_ENABLED, IOS_RECORDING_ENABLED, getFeatureFlag } from '../../../base/flags';
9 9
 import { connect } from '../../../base/redux';
10 10
 import { StyleType } from '../../../base/styles';
@@ -34,6 +34,11 @@ type Props = {
34 34
      */
35 35
     _chatEnabled: boolean,
36 36
 
37
+    /**
38
+     * True if the overflow menu is currently visible, false otherwise.
39
+     */
40
+    _isOpen: boolean,
41
+
37 42
     /**
38 43
      * Whether the recoding button should be enabled or not.
39 44
      */
@@ -107,16 +112,22 @@ class OverflowMenu extends Component<Props> {
107 112
         );
108 113
     }
109 114
 
110
-    _onCancel: () => void;
115
+    _onCancel: () => boolean;
111 116
 
112 117
     /**
113 118
      * Hides this {@code OverflowMenu}.
114 119
      *
115 120
      * @private
116
-     * @returns {void}
121
+     * @returns {boolean}
117 122
      */
118 123
     _onCancel() {
119
-        this.props.dispatch(hideDialog(OverflowMenu_));
124
+        if (this.props._isOpen) {
125
+            this.props.dispatch(hideDialog(OverflowMenu_));
126
+
127
+            return true;
128
+        }
129
+
130
+        return false;
120 131
     }
121 132
 }
122 133
 
@@ -125,17 +136,14 @@ class OverflowMenu extends Component<Props> {
125 136
  *
126 137
  * @param {Object} state - Redux state.
127 138
  * @private
128
- * @returns {{
129
- *      _bottomSheetStyles: StyleType,
130
- *      _chatEnabled: boolean,
131
- *      _recordingEnabled: boolean
132
- *  }}
139
+ * @returns {Props}
133 140
  */
134 141
 function _mapStateToProps(state) {
135 142
     return {
136 143
         _bottomSheetStyles:
137 144
             ColorSchemeRegistry.get(state, 'BottomSheet'),
138 145
         _chatEnabled: getFeatureFlag(state, CHAT_ENABLED, true),
146
+        _isOpen: isDialogOpen(state, OverflowMenu_),
139 147
         _recordingEnabled: Platform.OS !== 'ios' || getFeatureFlag(state, IOS_RECORDING_ENABLED)
140 148
     };
141 149
 }

Loading…
Cancel
Save