Browse Source

feature-flags: initial implementation

The welcomePageEnabled and pictureInPictureEnabled props on mobile have been
converted to feature flags.
master
Saúl Ibarra Corretgé 6 years ago
parent
commit
cf7b10d53d

+ 36
- 20
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetConferenceOptions.java View File

@@ -54,6 +54,11 @@ public class JitsiMeetConferenceOptions implements Parcelable {
54 54
      */
55 55
     private Bundle colorScheme;
56 56
 
57
+    /**
58
+     * Feature flags. See: https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
59
+     */
60
+    private Bundle featureFlags;
61
+
57 62
     /**
58 63
      * Set to {@code true} to join the conference with audio / video muted or to start in audio
59 64
      * only mode respectively.
@@ -62,12 +67,6 @@ public class JitsiMeetConferenceOptions implements Parcelable {
62 67
     private Boolean audioOnly;
63 68
     private Boolean videoMuted;
64 69
 
65
-    /**
66
-     * Set to {@code true} to enable the welcome page. Typically SDK users won't need this enabled
67
-     * since the host application decides which meeting to join.
68
-     */
69
-    private Boolean welcomePageEnabled;
70
-
71 70
     /**
72 71
      * Class used to build the immutable {@link JitsiMeetConferenceOptions} object.
73 72
      */
@@ -78,14 +77,14 @@ public class JitsiMeetConferenceOptions implements Parcelable {
78 77
         private String token;
79 78
 
80 79
         private Bundle colorScheme;
80
+        private Bundle featureFlags;
81 81
 
82 82
         private Boolean audioMuted;
83 83
         private Boolean audioOnly;
84 84
         private Boolean videoMuted;
85 85
 
86
-        private Boolean welcomePageEnabled;
87
-
88 86
         public Builder() {
87
+            featureFlags = new Bundle();
89 88
         }
90 89
 
91 90
         /**\
@@ -186,7 +185,25 @@ public class JitsiMeetConferenceOptions implements Parcelable {
186 185
          * @return - The {@link Builder} object itself so the method calls can be chained.
187 186
          */
188 187
         public Builder setWelcomePageEnabled(boolean enabled) {
189
-            this.welcomePageEnabled = enabled;
188
+            this.featureFlags.putBoolean("welcomepage.enabled", enabled);
189
+
190
+            return this;
191
+        }
192
+
193
+        public Builder setFeatureFlag(String flag, boolean value) {
194
+            this.featureFlags.putBoolean(flag, value);
195
+
196
+            return this;
197
+        }
198
+
199
+        public Builder setFeatureFlag(String flag, String value) {
200
+            this.featureFlags.putString(flag, value);
201
+
202
+            return this;
203
+        }
204
+
205
+        public Builder setFeatureFlag(String flag, int value) {
206
+            this.featureFlags.putInt(flag, value);
190 207
 
191 208
             return this;
192 209
         }
@@ -204,10 +221,10 @@ public class JitsiMeetConferenceOptions implements Parcelable {
204 221
             options.subject = this.subject;
205 222
             options.token = this.token;
206 223
             options.colorScheme = this.colorScheme;
224
+            options.featureFlags = this.featureFlags;
207 225
             options.audioMuted = this.audioMuted;
208 226
             options.audioOnly = this.audioOnly;
209 227
             options.videoMuted = this.videoMuted;
210
-            options.welcomePageEnabled = this.welcomePageEnabled;
211 228
 
212 229
             return options;
213 230
         }
@@ -221,29 +238,28 @@ public class JitsiMeetConferenceOptions implements Parcelable {
221 238
         subject = in.readString();
222 239
         token = in.readString();
223 240
         colorScheme = in.readBundle();
241
+        featureFlags = in.readBundle();
224 242
         byte tmpAudioMuted = in.readByte();
225 243
         audioMuted = tmpAudioMuted == 0 ? null : tmpAudioMuted == 1;
226 244
         byte tmpAudioOnly = in.readByte();
227 245
         audioOnly = tmpAudioOnly == 0 ? null : tmpAudioOnly == 1;
228 246
         byte tmpVideoMuted = in.readByte();
229 247
         videoMuted = tmpVideoMuted == 0 ? null : tmpVideoMuted == 1;
230
-        byte tmpWelcomePageEnabled = in.readByte();
231
-        welcomePageEnabled = tmpWelcomePageEnabled == 0 ? null : tmpWelcomePageEnabled == 1;
232 248
     }
233 249
 
234 250
     Bundle asProps() {
235 251
         Bundle props = new Bundle();
236 252
 
237
-        if (colorScheme != null) {
238
-            props.putBundle("colorScheme", colorScheme);
253
+        // Android always has the PiP flag set by default.
254
+        if (!featureFlags.containsKey("pip.enabled")) {
255
+            featureFlags.putBoolean("pip.enabled", true);
239 256
         }
240 257
 
241
-        if (welcomePageEnabled != null) {
242
-            props.putBoolean("welcomePageEnabled", welcomePageEnabled);
243
-        }
258
+        props.putBundle("flags", featureFlags);
244 259
 
245
-        // TODO: get rid of this.
246
-        props.putBoolean("pictureInPictureEnabled", true);
260
+        if (colorScheme != null) {
261
+            props.putBundle("colorScheme", colorScheme);
262
+        }
247 263
 
248 264
         Bundle config = new Bundle();
249 265
 
@@ -305,10 +321,10 @@ public class JitsiMeetConferenceOptions implements Parcelable {
305 321
         dest.writeString(subject);
306 322
         dest.writeString(token);
307 323
         dest.writeBundle(colorScheme);
324
+        dest.writeBundle(featureFlags);
308 325
         dest.writeByte((byte) (audioMuted == null ? 0 : audioMuted ? 1 : 2));
309 326
         dest.writeByte((byte) (audioOnly == null ? 0 : audioOnly ? 1 : 2));
310 327
         dest.writeByte((byte) (videoMuted == null ? 0 : videoMuted ? 1 : 2));
311
-        dest.writeByte((byte) (welcomePageEnabled == null ? 0 : welcomePageEnabled ? 1 : 2));
312 328
     }
313 329
 
314 330
     @Override

+ 6
- 0
ios/app/src/ViewController.m View File

@@ -96,4 +96,10 @@
96 96
     [self _onJitsiMeetViewDelegateEvent:@"CONFERENCE_WILL_JOIN" withData:data];
97 97
 }
98 98
 
99
+#if 0
100
+- (void)enterPictureInPicture:(NSDictionary *)data {
101
+    [self _onJitsiMeetViewDelegateEvent:@"ENTER_PICTURE_IN_PICTURE" withData:data];
102
+}
103
+#endif
104
+
99 105
 @end

+ 9
- 0
ios/sdk/src/JitsiMeetConferenceOptions.h View File

@@ -41,6 +41,11 @@
41 41
  */
42 42
 @property (nonatomic, copy, nullable) NSDictionary *colorScheme;
43 43
 
44
+/**
45
+ * Feature flags. See: https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
46
+ */
47
+@property (nonatomic, readonly, nonnull) NSDictionary *featureFlags;
48
+
44 49
 /**
45 50
  * Set to YES to join the conference with audio / video muted or to start in audio
46 51
  * only mode respectively.
@@ -55,6 +60,9 @@
55 60
  */
56 61
 @property (nonatomic) BOOL welcomePageEnabled;
57 62
 
63
+- (void)setFeatureFlag:(NSString *_Nonnull)flag withBoolean:(BOOL)value;
64
+- (void)setFeatureFlag:(NSString *_Nonnull)flag withValue:(id _Nonnull)value;
65
+
58 66
 @end
59 67
 
60 68
 @interface JitsiMeetConferenceOptions : NSObject
@@ -66,6 +74,7 @@
66 74
 @property (nonatomic, copy, nullable, readonly) NSString *token;
67 75
 
68 76
 @property (nonatomic, copy, nullable) NSDictionary *colorScheme;
77
+@property (nonatomic, readonly, nonnull) NSDictionary *featureFlags;
69 78
 
70 79
 @property (nonatomic, readonly) BOOL audioOnly;
71 80
 @property (nonatomic, readonly) BOOL audioMuted;

+ 28
- 16
ios/sdk/src/JitsiMeetConferenceOptions.m View File

@@ -18,11 +18,17 @@
18 18
 
19 19
 #import "JitsiMeetConferenceOptions+Private.h"
20 20
 
21
+/**
22
+ * Backwards compatibility: turn the boolean property into a feature flag.
23
+ */
24
+static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
25
+
26
+
21 27
 @implementation JitsiMeetConferenceOptionsBuilder {
22 28
     NSNumber *_audioOnly;
23 29
     NSNumber *_audioMuted;
24 30
     NSNumber *_videoMuted;
25
-    NSNumber *_welcomePageEnabled;
31
+    NSMutableDictionary *_featureFlags;
26 32
 }
27 33
 
28 34
 @dynamic audioOnly;
@@ -38,17 +44,24 @@
38 44
         _token = nil;
39 45
 
40 46
         _colorScheme = nil;
47
+        _featureFlags = [[NSMutableDictionary alloc] init];
41 48
 
42 49
         _audioOnly = nil;
43 50
         _audioMuted = nil;
44 51
         _videoMuted = nil;
45
-
46
-        _welcomePageEnabled = nil;
47 52
     }
48 53
     
49 54
     return self;
50 55
 }
51 56
 
57
+- (void)setFeatureFlag:(NSString *)flag withBoolean:(BOOL)value {
58
+    [self setFeatureFlag:flag withValue:[NSNumber numberWithBool:value]];
59
+}
60
+
61
+- (void)setFeatureFlag:(NSString *)flag withValue:(id)value {
62
+    _featureFlags[flag] = value;
63
+}
64
+
52 65
 #pragma mark - Dynamic properties
53 66
 
54 67
 - (void)setAudioOnly:(BOOL)audioOnly {
@@ -76,11 +89,14 @@
76 89
 }
77 90
 
78 91
 - (void)setWelcomePageEnabled:(BOOL)welcomePageEnabled {
79
-    _welcomePageEnabled = [NSNumber numberWithBool:welcomePageEnabled];
92
+    [self setFeatureFlag:WelcomePageEnabledFeatureFlag
93
+               withBoolean:welcomePageEnabled];
80 94
 }
81 95
 
82 96
 - (BOOL)welcomePageEnabled {
83
-    return _welcomePageEnabled && [_welcomePageEnabled boolValue];
97
+    NSNumber *n = _featureFlags[WelcomePageEnabledFeatureFlag];
98
+
99
+    return n != nil ? [n boolValue] : NO;
84 100
 }
85 101
 
86 102
 #pragma mark - Private API
@@ -97,17 +113,13 @@
97 113
     return _videoMuted;
98 114
 }
99 115
 
100
-- (NSNumber *)getWelcomePageEnabled {
101
-    return _welcomePageEnabled;
102
-}
103
-
104 116
 @end
105 117
 
106 118
 @implementation JitsiMeetConferenceOptions {
107 119
     NSNumber *_audioOnly;
108 120
     NSNumber *_audioMuted;
109 121
     NSNumber *_videoMuted;
110
-    NSNumber *_welcomePageEnabled;
122
+    NSDictionary *_featureFlags;
111 123
 }
112 124
 
113 125
 @dynamic audioOnly;
@@ -130,7 +142,9 @@
130 142
 }
131 143
 
132 144
 - (BOOL)welcomePageEnabled {
133
-    return _welcomePageEnabled && [_welcomePageEnabled boolValue];
145
+    NSNumber *n = _featureFlags[WelcomePageEnabledFeatureFlag];
146
+
147
+    return n != nil ? [n boolValue] : NO;
134 148
 }
135 149
 
136 150
 #pragma mark - Internal initializer
@@ -148,7 +162,7 @@
148 162
         _audioMuted = [builder getAudioMuted];
149 163
         _videoMuted = [builder getVideoMuted];
150 164
 
151
-        _welcomePageEnabled = [builder getWelcomePageEnabled];
165
+        _featureFlags = [NSDictionary dictionaryWithDictionary:builder.featureFlags];
152 166
     }
153 167
 
154 168
     return self;
@@ -167,14 +181,12 @@
167 181
 - (NSDictionary *)asProps {
168 182
     NSMutableDictionary *props = [[NSMutableDictionary alloc] init];
169 183
 
184
+    props[@"flags"] = [NSMutableDictionary dictionaryWithDictionary:_featureFlags];
185
+
170 186
     if (_colorScheme != nil) {
171 187
         props[@"colorScheme"] = self.colorScheme;
172 188
     }
173 189
 
174
-    if (_welcomePageEnabled != nil) {
175
-        props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
176
-    }
177
-
178 190
     NSMutableDictionary *config = [[NSMutableDictionary alloc] init];
179 191
     if (_audioOnly != nil) {
180 192
         config[@"startAudioOnly"] = @(self.audioOnly);

+ 14
- 4
ios/sdk/src/JitsiMeetView.m View File

@@ -24,6 +24,12 @@
24 24
 #import "RNRootView.h"
25 25
 
26 26
 
27
+/**
28
+ * Backwards compatibility: turn the boolean prop into a feature flag.
29
+ */
30
+static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
31
+
32
+
27 33
 @implementation JitsiMeetView {
28 34
     /**
29 35
      * The unique identifier of this `JitsiMeetView` within the process for the
@@ -122,11 +128,15 @@ static void initializeViewsMap() {
122 128
 - (void)setProps:(NSDictionary *_Nonnull)newProps {
123 129
     NSMutableDictionary *props = mergeProps([[JitsiMeet sharedInstance] getDefaultProps], newProps);
124 130
 
125
-    props[@"externalAPIScope"] = externalAPIScope;
131
+    // Set the PiP flag if it wasn't manually set.
132
+    NSMutableDictionary *featureFlags = props[@"flags"];
133
+    if (featureFlags[PiPEnabledFeatureFlag] == nil) {
134
+        featureFlags[PiPEnabledFeatureFlag]
135
+            = [NSNumber numberWithBool:
136
+               self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]];
137
+    }
126 138
 
127
-    // TODO: put this in some 'flags' field
128
-    props[@"pictureInPictureEnabled"]
129
-        = @(self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]);
139
+    props[@"externalAPIScope"] = externalAPIScope;
130 140
 
131 141
     // This method is supposed to be imperative i.e. a second
132 142
     // invocation with one and the same URL is expected to join the respective

+ 6
- 11
react/features/app/components/App.native.js View File

@@ -6,6 +6,7 @@ import '../../analytics';
6 6
 import '../../authentication';
7 7
 import { setColorScheme } from '../../base/color-scheme';
8 8
 import { DialogContainer } from '../../base/dialog';
9
+import { updateFlags } from '../../base/flags';
9 10
 import '../../base/jwt';
10 11
 import { Platform } from '../../base/react';
11 12
 import {
@@ -47,18 +48,9 @@ type Props = AbstractAppProps & {
47 48
     externalAPIScope: string,
48 49
 
49 50
     /**
50
-     * Whether Picture-in-Picture is enabled. If {@code true}, a toolbar button
51
-     * is rendered in the {@link Conference} view to afford entering
52
-     * Picture-in-Picture.
51
+     * An object with the feature flags.
53 52
      */
54
-    pictureInPictureEnabled: boolean,
55
-
56
-    /**
57
-     * Whether the Welcome page is enabled. If {@code true}, the Welcome page is
58
-     * rendered when the {@link App} is not at a location (URL) identifying
59
-     * a Jitsi Meet conference/room.
60
-     */
61
-    welcomePageEnabled: boolean
53
+    flags: Object
62 54
 };
63 55
 
64 56
 /**
@@ -99,6 +91,9 @@ export class App extends AbstractApp {
99 91
             // We set the color scheme early enough so then we avoid any
100 92
             // unnecessary re-renders.
101 93
             this.state.store.dispatch(setColorScheme(this.props.colorScheme));
94
+
95
+            // Ditto for feature flags.
96
+            this.state.store.dispatch(updateFlags(this.props.flags));
102 97
         });
103 98
     }
104 99
 

+ 10
- 0
react/features/base/flags/actionTypes.js View File

@@ -0,0 +1,10 @@
1
+/**
2
+ * The type of Redux action which updates the feature flags.
3
+ *
4
+ * {
5
+ *     type: UPDATE_FLAGS,
6
+ *     flags: Object
7
+ * }
8
+ *
9
+ */
10
+export const UPDATE_FLAGS = 'UPDATE_FLAGS';

+ 19
- 0
react/features/base/flags/actions.js View File

@@ -0,0 +1,19 @@
1
+// @flow
2
+
3
+import { UPDATE_FLAGS } from './actionTypes';
4
+
5
+/**
6
+ * Updates the current features flags with the given ones. They will be merged.
7
+ *
8
+ * @param {Object} flags - The new flags object.
9
+ * @returns {{
10
+ *     type: UPDATE_FLAGS,
11
+ *     flags: Object
12
+ * }}
13
+ */
14
+export function updateFlags(flags: Object) {
15
+    return {
16
+        type: UPDATE_FLAGS,
17
+        flags
18
+    };
19
+}

+ 13
- 0
react/features/base/flags/constants.js View File

@@ -0,0 +1,13 @@
1
+// @flow
2
+
3
+/**
4
+ * Flag indicating if Picture-in-Picture should be enabled.
5
+ * Default: auto-detected.
6
+ */
7
+export const PIP_ENABLED = 'pip.enabled';
8
+
9
+/**
10
+ * Flag indicating if the welcome page should be enabled.
11
+ * Default: disabled (false).
12
+ */
13
+export const WELCOME_PAGE_ENABLED = 'welcomepage.enabled';

+ 32
- 0
react/features/base/flags/functions.js View File

@@ -0,0 +1,32 @@
1
+// @flow
2
+
3
+import { getAppProp } from '../app';
4
+import { toState } from '../redux';
5
+
6
+/**
7
+ * Gets the value of a specific feature flag.
8
+ *
9
+ * @param {Function|Object} stateful - The redux store or {@code getState}
10
+ * function.
11
+ * @param {string} flag - The name of the React {@code Component} prop of
12
+ * the currently mounted {@code App} to get.
13
+ * @param {*} defaultValue - A default value for the flag, in case it's not defined.
14
+ * @returns {*} The value of the specified React {@code Compoennt} prop of the
15
+ * currently mounted {@code App}.
16
+ */
17
+export function getFeatureFlag(stateful: Function | Object, flag: string, defaultValue: any) {
18
+    const state = toState(stateful)['features/base/flags'];
19
+
20
+    if (state) {
21
+        const value = state[flag];
22
+
23
+        if (typeof value !== 'undefined') {
24
+            return value;
25
+        }
26
+    }
27
+
28
+    // Maybe the value hasn't made it to the redux store yet, check the app props.
29
+    const flags = getAppProp(stateful, 'flags') || {};
30
+
31
+    return flags[flag] || defaultValue;
32
+}

+ 6
- 0
react/features/base/flags/index.js View File

@@ -0,0 +1,6 @@
1
+export * from './actions';
2
+export * from './actionTypes';
3
+export * from './constants';
4
+export * from './functions';
5
+
6
+import './reducer';

+ 33
- 0
react/features/base/flags/reducer.js View File

@@ -0,0 +1,33 @@
1
+// @flow
2
+
3
+import _ from 'lodash';
4
+
5
+import { ReducerRegistry } from '../redux';
6
+
7
+import { UPDATE_FLAGS } from './actionTypes';
8
+
9
+/**
10
+ * Default state value for the feature flags.
11
+ */
12
+const DEFAULT_STATE = {};
13
+
14
+/**
15
+ * Reduces redux actions which handle feature flags.
16
+ *
17
+ * @param {State} state - The current redux state.
18
+ * @param {Action} action - The redux action to reduce.
19
+ * @param {string} action.type - The type of the redux action to reduce.
20
+ * @returns {State} The next redux state that is the result of reducing the
21
+ * specified action.
22
+ */
23
+ReducerRegistry.register('features/base/flags', (state = DEFAULT_STATE, action) => {
24
+    switch (action.type) {
25
+    case UPDATE_FLAGS: {
26
+        const newState = _.merge({}, state, action.flags);
27
+
28
+        return _.isEqual(state, newState) ? state : newState;
29
+    }
30
+    }
31
+
32
+    return state;
33
+});

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

@@ -4,7 +4,7 @@ import React from 'react';
4 4
 import { BackHandler, NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
5 5
 
6 6
 import { appNavigate } from '../../../app';
7
-import { getAppProp } from '../../../base/app';
7
+import { PIP_ENABLED, getFeatureFlag } from '../../../base/flags';
8 8
 import { getParticipantCount } from '../../../base/participants';
9 9
 import { Container, LoadingIndicator, TintedView } from '../../../base/react';
10 10
 import { connect } from '../../../base/redux';
@@ -452,7 +452,7 @@ function _mapStateToProps(state) {
452 452
          * @private
453 453
          * @type {boolean}
454 454
          */
455
-        _pictureInPictureEnabled: getAppProp(state, 'pictureInPictureEnabled'),
455
+        _pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
456 456
 
457 457
         /**
458 458
          * The indicator which determines whether the UI is reduced (to

+ 2
- 2
react/features/mobile/picture-in-picture/actions.js View File

@@ -3,7 +3,7 @@
3 3
 import { NativeModules } from 'react-native';
4 4
 import type { Dispatch } from 'redux';
5 5
 
6
-import { getAppProp } from '../../base/app';
6
+import { PIP_ENABLED, getFeatureFlag } from '../../base/flags';
7 7
 import { Platform } from '../../base/react';
8 8
 
9 9
 import { ENTER_PICTURE_IN_PICTURE } from './actionTypes';
@@ -25,7 +25,7 @@ export function enterPictureInPicture() {
25 25
         // XXX At the time of this writing this action can only be dispatched by
26 26
         // the button which is on the conference view, which means that it's
27 27
         // fine to enter PiP mode.
28
-        if (getAppProp(getState, 'pictureInPictureEnabled')) {
28
+        if (getFeatureFlag(getState, PIP_ENABLED)) {
29 29
             const { PictureInPicture } = NativeModules;
30 30
             const p
31 31
                 = Platform.OS === 'android'

+ 2
- 2
react/features/mobile/picture-in-picture/components/PictureInPictureButton.js View File

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { getAppProp } from '../../../base/app';
3
+import { PIP_ENABLED, getFeatureFlag } from '../../../base/flags';
4 4
 import { translate } from '../../../base/i18n';
5 5
 import { connect } from '../../../base/redux';
6 6
 import { AbstractButton } from '../../../base/toolbox';
@@ -62,7 +62,7 @@ class PictureInPictureButton extends AbstractButton<Props, *> {
62 62
  */
63 63
 function _mapStateToProps(state): Object {
64 64
     return {
65
-        _enabled: Boolean(getAppProp(state, 'pictureInPictureEnabled'))
65
+        _enabled: Boolean(getFeatureFlag(state, PIP_ENABLED))
66 66
     };
67 67
 }
68 68
 

+ 2
- 2
react/features/welcome/functions.js View File

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { getAppProp } from '../base/app';
3
+import { WELCOME_PAGE_ENABLED, getFeatureFlag } from '../base/flags';
4 4
 import { toState } from '../base/redux';
5 5
 
6 6
 declare var APP: Object;
@@ -24,7 +24,7 @@ export function isWelcomePageAppEnabled(stateful: Function | Object) {
24 24
         // - Enabling/disabling the Welcome page on Web historically
25 25
         // automatically redirects to a random room and that does not make sense
26 26
         // on mobile (right now).
27
-        return Boolean(getAppProp(stateful, 'welcomePageEnabled'));
27
+        return Boolean(getFeatureFlag(stateful, WELCOME_PAGE_ENABLED));
28 28
     }
29 29
 
30 30
     return true;

Loading…
Cancel
Save