浏览代码

Singleton follow me (#4144)

* Prints errors in case of wrong initialization.

Not printing can masks some errors in the code.

* Allow only one Follow Me moderator in a meeting.

* Sends Follow Me state with all presences of the moderator.

This fixes an issue where the moderator sends the Follow Me state and then for example mute or unmute video (this will produce a presence without Follow Me state) and the new comers will not reflect current Follow Me state till a change of it comes.

* Changes fixing comments.

* Changes fixing comments.
j8
Дамян Минков 6 年前
父节点
当前提交
a6555c5d24
没有帐户链接到提交者的电子邮件

+ 10
- 2
react/features/base/app/components/BaseApp.js 查看文件

@@ -18,6 +18,8 @@ import { PersistenceRegistry } from '../../storage';
18 18
 
19 19
 import { appWillMount, appWillUnmount } from '../actions';
20 20
 
21
+const logger = require('jitsi-meet-logger').getLogger(__filename);
22
+
21 23
 declare var APP: Object;
22 24
 
23 25
 /**
@@ -74,14 +76,20 @@ export default class BaseApp extends Component<*, State> {
74 76
          * @type {Promise}
75 77
          */
76 78
         this._init = this._initStorage()
77
-            .catch(() => { /* BaseApp should always initialize! */ })
79
+            .catch(err => {
80
+                /* BaseApp should always initialize! */
81
+                logger.error(err);
82
+            })
78 83
             .then(() => new Promise(resolve => {
79 84
                 this.setState({
80 85
                     store: this._createStore()
81 86
                 }, resolve);
82 87
             }))
83 88
             .then(() => this.state.store.dispatch(appWillMount(this)))
84
-            .catch(() => { /* BaseApp should always initialize! */ });
89
+            .catch(err => {
90
+                /* BaseApp should always initialize! */
91
+                logger.error(err);
92
+            });
85 93
     }
86 94
 
87 95
     /**

+ 23
- 0
react/features/follow-me/actionTypes.js 查看文件

@@ -0,0 +1,23 @@
1
+// @flow
2
+
3
+/**
4
+ * The id of the Follow Me moderator.
5
+ *
6
+ * {
7
+ *     type: SET_FOLLOW_ME_MODERATOR,
8
+ *     id: boolean
9
+ * }
10
+ */
11
+export const SET_FOLLOW_ME_MODERATOR = 'SET_FOLLOW_ME_MODERATOR';
12
+
13
+/**
14
+ * The type of (redux) action which updates the current known state of the
15
+ * Follow Me feature.
16
+ *
17
+ *
18
+ * {
19
+ *     type: SET_FOLLOW_ME_STATE,
20
+ *     state: boolean
21
+ * }
22
+ */
23
+export const SET_FOLLOW_ME_STATE = 'SET_FOLLOW_ME_STATE';

+ 38
- 0
react/features/follow-me/actions.js 查看文件

@@ -0,0 +1,38 @@
1
+// @flow
2
+
3
+import {
4
+    SET_FOLLOW_ME_MODERATOR,
5
+    SET_FOLLOW_ME_STATE
6
+} from './actionTypes';
7
+
8
+/**
9
+ * Sets the current moderator id or clears it.
10
+ *
11
+ * @param {?string} id - The Follow Me moderator participant id.
12
+ * @returns {{
13
+ *     type: SET_FOLLOW_ME_MODERATOR,
14
+ *     id, string
15
+ * }}
16
+ */
17
+export function setFollowMeModerator(id: ?string) {
18
+    return {
19
+        type: SET_FOLLOW_ME_MODERATOR,
20
+        id
21
+    };
22
+}
23
+
24
+/**
25
+ * Sets the Follow Me feature state.
26
+ *
27
+ * @param {?Object} state - The current state.
28
+ * @returns {{
29
+ *     type: SET_FOLLOW_ME_STATE,
30
+ *     state: Object
31
+ * }}
32
+ */
33
+export function setFollowMeState(state: ?Object) {
34
+    return {
35
+        type: SET_FOLLOW_ME_STATE,
36
+        state
37
+    };
38
+}

+ 2
- 0
react/features/follow-me/index.js 查看文件

@@ -1,2 +1,4 @@
1 1
 export * from './middleware';
2 2
 export * from './subscriber';
3
+
4
+import './reducer';

+ 38
- 4
react/features/follow-me/middleware.js 查看文件

@@ -1,9 +1,14 @@
1 1
 // @flow
2 2
 
3
+import {
4
+    setFollowMeModerator,
5
+    setFollowMeState
6
+} from './actions';
3 7
 import { CONFERENCE_WILL_JOIN } from '../base/conference';
4 8
 import {
5 9
     getParticipantById,
6 10
     getPinnedParticipant,
11
+    PARTICIPANT_LEFT,
7 12
     pinParticipant
8 13
 } from '../base/participants';
9 14
 import { MiddlewareRegistry } from '../base/redux';
@@ -58,7 +63,13 @@ MiddlewareRegistry.register(store => next => action => {
58 63
             FOLLOW_ME_COMMAND, ({ attributes }, id) => {
59 64
                 _onFollowMeCommand(attributes, id, store);
60 65
             });
66
+        break;
61 67
     }
68
+    case PARTICIPANT_LEFT:
69
+        if (store.getState()['features/follow-me'].moderator === action.participant.id) {
70
+            store.dispatch(setFollowMeModerator());
71
+        }
72
+        break;
62 73
     }
63 74
 
64 75
     return next(action);
@@ -101,14 +112,36 @@ function _onFollowMeCommand(attributes = {}, id, store) {
101 112
         return;
102 113
     }
103 114
 
115
+    if (!state['features/follow-me'].moderator) {
116
+        store.dispatch(setFollowMeModerator(id));
117
+    }
118
+
119
+    // just a command that follow me was turned off
120
+    if (attributes.off) {
121
+        store.dispatch(setFollowMeModerator());
122
+
123
+        return;
124
+    }
125
+
126
+    const oldState = state['features/follow-me'].state || {};
127
+
128
+    store.dispatch(setFollowMeState(attributes));
129
+
104 130
     // XMPP will translate all booleans to strings, so explicitly check against
105 131
     // the string form of the boolean {@code true}.
106
-    store.dispatch(setFilmstripVisible(attributes.filmstripVisible === 'true'));
107
-    store.dispatch(setTileView(attributes.tileViewEnabled === 'true'));
132
+    if (oldState.filmstripVisible !== attributes.filmstripVisible) {
133
+        store.dispatch(setFilmstripVisible(attributes.filmstripVisible === 'true'));
134
+    }
135
+
136
+    if (oldState.tileViewEnabled !== attributes.tileViewEnabled) {
137
+        store.dispatch(setTileView(attributes.tileViewEnabled === 'true'));
138
+    }
108 139
 
109 140
     // For now gate etherpad checks behind a web-app check to be extra safe
110 141
     // against calling a web-app global.
111
-    if (typeof APP !== 'undefined' && state['features/etherpad'].initialized) {
142
+    if (typeof APP !== 'undefined'
143
+        && state['features/etherpad'].initialized
144
+        && oldState.sharedDocumentVisible !== attributes.sharedDocumentVisible) {
112 145
         const isEtherpadVisible = attributes.sharedDocumentVisible === 'true';
113 146
         const documentManager = APP.UI.getSharedDocumentManager();
114 147
 
@@ -124,7 +157,8 @@ function _onFollowMeCommand(attributes = {}, id, store) {
124 157
 
125 158
     if (typeof idOfParticipantToPin !== 'undefined'
126 159
             && (!pinnedParticipant
127
-                || idOfParticipantToPin !== pinnedParticipant.id)) {
160
+                || idOfParticipantToPin !== pinnedParticipant.id)
161
+            && oldState.nextOnStage !== attributes.nextOnStage) {
128 162
         _pinVideoThumbnailById(store, idOfParticipantToPin);
129 163
     } else if (typeof idOfParticipantToPin === 'undefined'
130 164
             && pinnedParticipant) {

+ 33
- 0
react/features/follow-me/reducer.js 查看文件

@@ -0,0 +1,33 @@
1
+// @flow
2
+
3
+import {
4
+    SET_FOLLOW_ME_MODERATOR,
5
+    SET_FOLLOW_ME_STATE
6
+} from './actionTypes';
7
+import { ReducerRegistry, set } from '../base/redux';
8
+
9
+/**
10
+ * Listen for actions that contain the Follow Me feature active state, so that it can be stored.
11
+ */
12
+ReducerRegistry.register(
13
+    'features/follow-me',
14
+    (state = {}, action) => {
15
+        switch (action.type) {
16
+
17
+        case SET_FOLLOW_ME_MODERATOR: {
18
+            let newState = set(state, 'moderator', action.id);
19
+
20
+            if (!action.id) {
21
+                // clear the state if feature becomes disabled
22
+                newState = set(newState, 'state', undefined);
23
+            }
24
+
25
+            return newState;
26
+        }
27
+        case SET_FOLLOW_ME_STATE: {
28
+            return set(state, 'state', action.state);
29
+        }
30
+        }
31
+
32
+        return state;
33
+    });

+ 19
- 10
react/features/follow-me/subscriber.js 查看文件

@@ -12,13 +12,12 @@ import { FOLLOW_ME_COMMAND } from './constants';
12 12
 /**
13 13
  * Subscribes to changes to the Follow Me setting for the local participant to
14 14
  * notify remote participants of current user interface status.
15
- *
16
- * @param sharedDocumentVisible {Boolean} {true} if the shared document was
17
- * shown (as a result of the toggle) or {false} if it was hidden
15
+ * Changing newSelectedValue param to off, when feature is turned of so we can
16
+ * notify all listeners.
18 17
  */
19 18
 StateListenerRegistry.register(
20 19
     /* selector */ state => state['features/base/conference'].followMeEnabled,
21
-    /* listener */ _sendFollowMeCommand);
20
+    /* listener */ (newSelectedValue, store) => _sendFollowMeCommand(newSelectedValue || 'off', store));
22 21
 
23 22
 /**
24 23
  * Subscribes to changes to the currently pinned participant in the user
@@ -90,7 +89,7 @@ function _sendFollowMeCommand(
90 89
     const state = store.getState();
91 90
     const conference = getCurrentConference(state);
92 91
 
93
-    if (!conference || !state['features/base/conference'].followMeEnabled) {
92
+    if (!conference) {
94 93
         return;
95 94
     }
96 95
 
@@ -99,11 +98,21 @@ function _sendFollowMeCommand(
99 98
         return;
100 99
     }
101 100
 
102
-    // XXX The "Follow Me" command represents a snapshot of all states
103
-    // which are to be followed so don't forget to removeCommand before
104
-    // sendCommand!
105
-    conference.removeCommand(FOLLOW_ME_COMMAND);
106
-    conference.sendCommandOnce(
101
+    if (newSelectedValue === 'off') {
102
+        // if the change is to off, local user turned off follow me and
103
+        // we want to signal this
104
+
105
+        conference.sendCommandOnce(
106
+            FOLLOW_ME_COMMAND,
107
+            { attributes: { off: true } }
108
+        );
109
+
110
+        return;
111
+    } else if (!state['features/base/conference'].followMeEnabled) {
112
+        return;
113
+    }
114
+
115
+    conference.sendCommand(
107 116
         FOLLOW_ME_COMMAND,
108 117
         { attributes: _getFollowMeState(state) }
109 118
     );

+ 8
- 1
react/features/settings/components/web/MoreTab.js 查看文件

@@ -23,6 +23,11 @@ export type Props = {
23 23
      */
24 24
     currentLanguage: string,
25 25
 
26
+    /**
27
+     * Whether or not follow me is currently active (enabled by some other participant).
28
+     */
29
+    followMeActive: boolean,
30
+
26 31
     /**
27 32
      * Whether or not the user has selected the Follow Me feature to be enabled.
28 33
      */
@@ -189,6 +194,7 @@ class MoreTab extends AbstractDialogTab<Props, State> {
189 194
      */
190 195
     _renderModeratorSettings() {
191 196
         const {
197
+            followMeActive,
192 198
             followMeEnabled,
193 199
             startAudioMuted,
194 200
             startVideoMuted,
@@ -221,7 +227,8 @@ class MoreTab extends AbstractDialogTab<Props, State> {
221 227
                             super._onChange({ startVideoMuted: checked })
222 228
                     } />
223 229
                 <Checkbox
224
-                    isChecked = { followMeEnabled }
230
+                    isChecked = { followMeEnabled && !followMeActive }
231
+                    isDisabled = { followMeActive }
225 232
                     label = { t('settings.followMe') }
226 233
                     name = 'follow-me'
227 234
                     // eslint-disable-next-line react/jsx-no-bind

+ 11
- 0
react/features/settings/components/web/SettingsDialog.js 查看文件

@@ -190,6 +190,17 @@ function _mapStateToProps(state) {
190 190
             component: MoreTab,
191 191
             label: 'settings.more',
192 192
             props: moreTabProps,
193
+            propsUpdateFunction: (tabState, newProps) => {
194
+                // Updates tab props, keeping users selection
195
+
196
+                return {
197
+                    ...newProps,
198
+                    currentLanguage: tabState.currentLanguage,
199
+                    followMeEnabled: tabState.followMeEnabled,
200
+                    startAudioMuted: tabState.startAudioMuted,
201
+                    startVideoMuted: tabState.startVideoMuted
202
+                };
203
+            },
193 204
             styles: 'settings-pane more-pane',
194 205
             submit: submitMoreTab
195 206
         });

+ 2
- 0
react/features/settings/functions.js 查看文件

@@ -81,6 +81,7 @@ export function getMoreTabProps(stateful: Object | Function) {
81 81
         startAudioMutedPolicy,
82 82
         startVideoMutedPolicy
83 83
     } = state['features/base/conference'];
84
+    const followMeActive = Boolean(state['features/follow-me'].moderator);
84 85
     const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
85 86
     const localParticipant = getLocalParticipant(state);
86 87
 
@@ -93,6 +94,7 @@ export function getMoreTabProps(stateful: Object | Function) {
93 94
 
94 95
     return {
95 96
         currentLanguage: language,
97
+        followMeActive: Boolean(conference && followMeActive),
96 98
         followMeEnabled: Boolean(conference && followMeEnabled),
97 99
         languages: LANGUAGES,
98 100
         showLanguageSettings: configuredTabs.includes('language'),

正在加载...
取消
保存