Browse Source

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.
master
Дамян Минков 6 years ago
parent
commit
a6555c5d24
No account linked to committer's email address

+ 10
- 2
react/features/base/app/components/BaseApp.js View File

18
 
18
 
19
 import { appWillMount, appWillUnmount } from '../actions';
19
 import { appWillMount, appWillUnmount } from '../actions';
20
 
20
 
21
+const logger = require('jitsi-meet-logger').getLogger(__filename);
22
+
21
 declare var APP: Object;
23
 declare var APP: Object;
22
 
24
 
23
 /**
25
 /**
74
          * @type {Promise}
76
          * @type {Promise}
75
          */
77
          */
76
         this._init = this._initStorage()
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
             .then(() => new Promise(resolve => {
83
             .then(() => new Promise(resolve => {
79
                 this.setState({
84
                 this.setState({
80
                     store: this._createStore()
85
                     store: this._createStore()
81
                 }, resolve);
86
                 }, resolve);
82
             }))
87
             }))
83
             .then(() => this.state.store.dispatch(appWillMount(this)))
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 View File

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 View File

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 View File

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

+ 38
- 4
react/features/follow-me/middleware.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
+import {
4
+    setFollowMeModerator,
5
+    setFollowMeState
6
+} from './actions';
3
 import { CONFERENCE_WILL_JOIN } from '../base/conference';
7
 import { CONFERENCE_WILL_JOIN } from '../base/conference';
4
 import {
8
 import {
5
     getParticipantById,
9
     getParticipantById,
6
     getPinnedParticipant,
10
     getPinnedParticipant,
11
+    PARTICIPANT_LEFT,
7
     pinParticipant
12
     pinParticipant
8
 } from '../base/participants';
13
 } from '../base/participants';
9
 import { MiddlewareRegistry } from '../base/redux';
14
 import { MiddlewareRegistry } from '../base/redux';
58
             FOLLOW_ME_COMMAND, ({ attributes }, id) => {
63
             FOLLOW_ME_COMMAND, ({ attributes }, id) => {
59
                 _onFollowMeCommand(attributes, id, store);
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
     return next(action);
75
     return next(action);
101
         return;
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
     // XMPP will translate all booleans to strings, so explicitly check against
130
     // XMPP will translate all booleans to strings, so explicitly check against
105
     // the string form of the boolean {@code true}.
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
     // For now gate etherpad checks behind a web-app check to be extra safe
140
     // For now gate etherpad checks behind a web-app check to be extra safe
110
     // against calling a web-app global.
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
         const isEtherpadVisible = attributes.sharedDocumentVisible === 'true';
145
         const isEtherpadVisible = attributes.sharedDocumentVisible === 'true';
113
         const documentManager = APP.UI.getSharedDocumentManager();
146
         const documentManager = APP.UI.getSharedDocumentManager();
114
 
147
 
124
 
157
 
125
     if (typeof idOfParticipantToPin !== 'undefined'
158
     if (typeof idOfParticipantToPin !== 'undefined'
126
             && (!pinnedParticipant
159
             && (!pinnedParticipant
127
-                || idOfParticipantToPin !== pinnedParticipant.id)) {
160
+                || idOfParticipantToPin !== pinnedParticipant.id)
161
+            && oldState.nextOnStage !== attributes.nextOnStage) {
128
         _pinVideoThumbnailById(store, idOfParticipantToPin);
162
         _pinVideoThumbnailById(store, idOfParticipantToPin);
129
     } else if (typeof idOfParticipantToPin === 'undefined'
163
     } else if (typeof idOfParticipantToPin === 'undefined'
130
             && pinnedParticipant) {
164
             && pinnedParticipant) {

+ 33
- 0
react/features/follow-me/reducer.js View File

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 View File

12
 /**
12
 /**
13
  * Subscribes to changes to the Follow Me setting for the local participant to
13
  * Subscribes to changes to the Follow Me setting for the local participant to
14
  * notify remote participants of current user interface status.
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
 StateListenerRegistry.register(
18
 StateListenerRegistry.register(
20
     /* selector */ state => state['features/base/conference'].followMeEnabled,
19
     /* selector */ state => state['features/base/conference'].followMeEnabled,
21
-    /* listener */ _sendFollowMeCommand);
20
+    /* listener */ (newSelectedValue, store) => _sendFollowMeCommand(newSelectedValue || 'off', store));
22
 
21
 
23
 /**
22
 /**
24
  * Subscribes to changes to the currently pinned participant in the user
23
  * Subscribes to changes to the currently pinned participant in the user
90
     const state = store.getState();
89
     const state = store.getState();
91
     const conference = getCurrentConference(state);
90
     const conference = getCurrentConference(state);
92
 
91
 
93
-    if (!conference || !state['features/base/conference'].followMeEnabled) {
92
+    if (!conference) {
94
         return;
93
         return;
95
     }
94
     }
96
 
95
 
99
         return;
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
         FOLLOW_ME_COMMAND,
116
         FOLLOW_ME_COMMAND,
108
         { attributes: _getFollowMeState(state) }
117
         { attributes: _getFollowMeState(state) }
109
     );
118
     );

+ 8
- 1
react/features/settings/components/web/MoreTab.js View File

23
      */
23
      */
24
     currentLanguage: string,
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
      * Whether or not the user has selected the Follow Me feature to be enabled.
32
      * Whether or not the user has selected the Follow Me feature to be enabled.
28
      */
33
      */
189
      */
194
      */
190
     _renderModeratorSettings() {
195
     _renderModeratorSettings() {
191
         const {
196
         const {
197
+            followMeActive,
192
             followMeEnabled,
198
             followMeEnabled,
193
             startAudioMuted,
199
             startAudioMuted,
194
             startVideoMuted,
200
             startVideoMuted,
221
                             super._onChange({ startVideoMuted: checked })
227
                             super._onChange({ startVideoMuted: checked })
222
                     } />
228
                     } />
223
                 <Checkbox
229
                 <Checkbox
224
-                    isChecked = { followMeEnabled }
230
+                    isChecked = { followMeEnabled && !followMeActive }
231
+                    isDisabled = { followMeActive }
225
                     label = { t('settings.followMe') }
232
                     label = { t('settings.followMe') }
226
                     name = 'follow-me'
233
                     name = 'follow-me'
227
                     // eslint-disable-next-line react/jsx-no-bind
234
                     // eslint-disable-next-line react/jsx-no-bind

+ 11
- 0
react/features/settings/components/web/SettingsDialog.js View File

190
             component: MoreTab,
190
             component: MoreTab,
191
             label: 'settings.more',
191
             label: 'settings.more',
192
             props: moreTabProps,
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
             styles: 'settings-pane more-pane',
204
             styles: 'settings-pane more-pane',
194
             submit: submitMoreTab
205
             submit: submitMoreTab
195
         });
206
         });

+ 2
- 0
react/features/settings/functions.js View File

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

Loading…
Cancel
Save