Browse Source

ref(follow-me): hook into redux (#3991)

Use subscribers to detect state change and emit those
out to other participants. Use middleware to register
the command listener.
master
virtuacoplenny 5 years ago
parent
commit
c7013f5c4b
No account linked to committer's email address

+ 0
- 546
modules/FollowMe.js View File

@@ -1,546 +0,0 @@
1
-/* global APP */
2
-
3
-/*
4
- * Copyright @ 2015 Atlassian Pty Ltd
5
- *
6
- * Licensed under the Apache License, Version 2.0 (the "License");
7
- * you may not use this file except in compliance with the License.
8
- * You may obtain a copy of the License at
9
- *
10
- *     http://www.apache.org/licenses/LICENSE-2.0
11
- *
12
- * Unless required by applicable law or agreed to in writing, software
13
- * distributed under the License is distributed on an "AS IS" BASIS,
14
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- * See the License for the specific language governing permissions and
16
- * limitations under the License.
17
- */
18
-const logger = require('jitsi-meet-logger').getLogger(__filename);
19
-
20
-import {
21
-    getPinnedParticipant,
22
-    pinParticipant
23
-} from '../react/features/base/participants';
24
-import { setTileView } from '../react/features/video-layout';
25
-import UIEvents from '../service/UI/UIEvents';
26
-import VideoLayout from './UI/videolayout/VideoLayout';
27
-
28
-/**
29
- * The (name of the) command which transports the state (represented by
30
- * {State} for the local state at the time of this writing) of a {FollowMe}
31
- * (instance) between participants.
32
- */
33
-const _COMMAND = 'follow-me';
34
-
35
-/**
36
- * The timeout after which a follow-me command that has been received will be
37
- * ignored if not consumed.
38
- *
39
- * @type {number} in seconds
40
- * @private
41
- */
42
-const _FOLLOW_ME_RECEIVED_TIMEOUT = 30;
43
-
44
-/**
45
- * Represents the set of {FollowMe}-related states (properties and their
46
- * respective values) which are to be followed by a participant. {FollowMe}
47
- * will send {_COMMAND} whenever a property of {State} changes (if the local
48
- * participant is in her right to issue such a command, of course).
49
- */
50
-class State {
51
-    /**
52
-     * Initializes a new {State} instance.
53
-     *
54
-     * @param propertyChangeCallback {Function} which is to be called when a
55
-     * property of the new instance has its value changed from an old value
56
-     * into a (different) new value. The function is supplied with the name of
57
-     * the property, the old value of the property before the change, and the
58
-     * new value of the property after the change.
59
-     */
60
-    constructor(propertyChangeCallback) {
61
-        this._propertyChangeCallback = propertyChangeCallback;
62
-    }
63
-
64
-    /**
65
-     *
66
-     */
67
-    get filmstripVisible() {
68
-        return this._filmstripVisible;
69
-    }
70
-
71
-    /**
72
-     *
73
-     */
74
-    set filmstripVisible(b) {
75
-        const oldValue = this._filmstripVisible;
76
-
77
-        if (oldValue !== b) {
78
-            this._filmstripVisible = b;
79
-            this._firePropertyChange('filmstripVisible', oldValue, b);
80
-        }
81
-    }
82
-
83
-    /**
84
-     *
85
-     */
86
-    get nextOnStage() {
87
-        return this._nextOnStage;
88
-    }
89
-
90
-    /**
91
-     *
92
-     */
93
-    set nextOnStage(id) {
94
-        const oldValue = this._nextOnStage;
95
-
96
-        if (oldValue !== id) {
97
-            this._nextOnStage = id;
98
-            this._firePropertyChange('nextOnStage', oldValue, id);
99
-        }
100
-    }
101
-
102
-    /**
103
-     *
104
-     */
105
-    get sharedDocumentVisible() {
106
-        return this._sharedDocumentVisible;
107
-    }
108
-
109
-    /**
110
-     *
111
-     */
112
-    set sharedDocumentVisible(b) {
113
-        const oldValue = this._sharedDocumentVisible;
114
-
115
-        if (oldValue !== b) {
116
-            this._sharedDocumentVisible = b;
117
-            this._firePropertyChange('sharedDocumentVisible', oldValue, b);
118
-        }
119
-    }
120
-
121
-    /**
122
-     * A getter for this object instance to know the state of tile view.
123
-     *
124
-     * @returns {boolean} True if tile view is enabled.
125
-     */
126
-    get tileViewEnabled() {
127
-        return this._tileViewEnabled;
128
-    }
129
-
130
-    /**
131
-     * A setter for {@link tileViewEnabled}. Fires a property change event for
132
-     * other participants to follow.
133
-     *
134
-     * @param {boolean} b - Whether or not tile view is enabled.
135
-     * @returns {void}
136
-     */
137
-    set tileViewEnabled(b) {
138
-        const oldValue = this._tileViewEnabled;
139
-
140
-        if (oldValue !== b) {
141
-            this._tileViewEnabled = b;
142
-            this._firePropertyChange('tileViewEnabled', oldValue, b);
143
-        }
144
-    }
145
-
146
-    /**
147
-     * Invokes {_propertyChangeCallback} to notify it that {property} had its
148
-     * value changed from {oldValue} to {newValue}.
149
-     *
150
-     * @param property the name of the property which had its value changed
151
-     * from {oldValue} to {newValue}
152
-     * @param oldValue the value of {property} before the change
153
-     * @param newValue the value of {property} after the change
154
-     */
155
-    _firePropertyChange(property, oldValue, newValue) {
156
-        const propertyChangeCallback = this._propertyChangeCallback;
157
-
158
-        if (propertyChangeCallback) {
159
-            propertyChangeCallback(property, oldValue, newValue);
160
-        }
161
-    }
162
-}
163
-
164
-/**
165
- * Represents the "Follow Me" feature which enables a moderator to
166
- * (partially) control the user experience/interface (e.g. filmstrip
167
- * visibility) of (other) non-moderator particiapnts.
168
- *
169
- * @author Lyubomir Marinov
170
- */
171
-class FollowMe {
172
-    /**
173
-     * Initializes a new {FollowMe} instance.
174
-     *
175
-     * @param conference the {conference} which is to transport
176
-     * {FollowMe}-related information between participants
177
-     * @param UI the {UI} which is the source (model/state) to be sent to
178
-     * remote participants if the local participant is the moderator or the
179
-     * destination (model/state) to receive from the remote moderator if the
180
-     * local participant is not the moderator
181
-     */
182
-    constructor(conference, UI) {
183
-        this._conference = conference;
184
-        this._UI = UI;
185
-        this.nextOnStageTimer = 0;
186
-
187
-        // The states of the local participant which are to be followed (by the
188
-        // remote participants when the local participant is in her right to
189
-        // issue such commands).
190
-        this._local = new State(this._localPropertyChange.bind(this));
191
-
192
-        // Listen to "Follow Me" commands. I'm not sure whether a moderator can
193
-        // (in lib-jitsi-meet and/or Meet) become a non-moderator. If that's
194
-        // possible, then it may be easiest to always listen to commands. The
195
-        // listener will validate received commands before acting on them.
196
-        conference.commands.addCommandListener(
197
-                _COMMAND,
198
-                this._onFollowMeCommand.bind(this));
199
-    }
200
-
201
-    /**
202
-     * Sets the current state of all follow-me properties, which will fire a
203
-     * localPropertyChangeEvent and trigger a send of the follow-me command.
204
-     * @private
205
-     */
206
-    _setFollowMeInitialState() {
207
-        this._filmstripToggled.bind(this, this._UI.isFilmstripVisible());
208
-
209
-        const pinnedId = VideoLayout.getPinnedId();
210
-
211
-        this._nextOnStage(pinnedId, Boolean(pinnedId));
212
-
213
-        // check whether shared document is enabled/initialized
214
-        if (this._UI.getSharedDocumentManager()) {
215
-            this._sharedDocumentToggled
216
-                .bind(this, this._UI.getSharedDocumentManager().isVisible());
217
-        }
218
-
219
-        this._tileViewToggled.bind(
220
-            this,
221
-            APP.store.getState()['features/video-layout'].tileViewEnabled);
222
-    }
223
-
224
-    /**
225
-     * Adds listeners for the UI states of the local participant which are
226
-     * to be followed (by the remote participants). A non-moderator (very
227
-     * likely) can become a moderator so it may be easiest to always track
228
-     * the states of interest.
229
-     * @private
230
-     */
231
-    _addFollowMeListeners() {
232
-        this.filmstripEventHandler = this._filmstripToggled.bind(this);
233
-        this._UI.addListener(UIEvents.TOGGLED_FILMSTRIP,
234
-                            this.filmstripEventHandler);
235
-
236
-        const self = this;
237
-
238
-        this.pinnedEndpointEventHandler = function(videoId, isPinned) {
239
-            self._nextOnStage(videoId, isPinned);
240
-        };
241
-        this._UI.addListener(UIEvents.PINNED_ENDPOINT,
242
-                            this.pinnedEndpointEventHandler);
243
-
244
-        this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
245
-        this._UI.addListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
246
-                            this.sharedDocEventHandler);
247
-
248
-        this.tileViewEventHandler = this._tileViewToggled.bind(this);
249
-        this._UI.addListener(UIEvents.TOGGLED_TILE_VIEW,
250
-              this.tileViewEventHandler);
251
-    }
252
-
253
-    /**
254
-     * Removes all follow me listeners.
255
-     * @private
256
-     */
257
-    _removeFollowMeListeners() {
258
-        this._UI.removeListener(UIEvents.TOGGLED_FILMSTRIP,
259
-                                this.filmstripEventHandler);
260
-        this._UI.removeListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
261
-                                this.sharedDocEventHandler);
262
-        this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
263
-                                this.pinnedEndpointEventHandler);
264
-        this._UI.removeListener(UIEvents.TOGGLED_TILE_VIEW,
265
-                                this.tileViewEventHandler);
266
-    }
267
-
268
-    /**
269
-     * Enables or disabled the follow me functionality
270
-     *
271
-     * @param enable {true} to enable the follow me functionality, {false} -
272
-     * to disable it
273
-     */
274
-    enableFollowMe(enable) {
275
-        if (enable) {
276
-            this._setFollowMeInitialState();
277
-            this._addFollowMeListeners();
278
-        } else {
279
-            this._removeFollowMeListeners();
280
-        }
281
-    }
282
-
283
-    /**
284
-     * Notifies this instance that the (visibility of the) filmstrip was
285
-     * toggled (in the user interface of the local participant).
286
-     *
287
-     * @param filmstripVisible {Boolean} {true} if the filmstrip was shown (as a
288
-     * result of the toggle) or {false} if the filmstrip was hidden
289
-     */
290
-    _filmstripToggled(filmstripVisible) {
291
-        this._local.filmstripVisible = filmstripVisible;
292
-    }
293
-
294
-    /**
295
-     * Notifies this instance that the (visibility of the) shared document was
296
-     * toggled (in the user interface of the local participant).
297
-     *
298
-     * @param sharedDocumentVisible {Boolean} {true} if the shared document was
299
-     * shown (as a result of the toggle) or {false} if it was hidden
300
-     */
301
-    _sharedDocumentToggled(sharedDocumentVisible) {
302
-        this._local.sharedDocumentVisible = sharedDocumentVisible;
303
-    }
304
-
305
-    /**
306
-     * Notifies this instance that the tile view mode has been enabled or
307
-     * disabled.
308
-     *
309
-     * @param {boolean} enabled - True if tile view has been enabled, false
310
-     * if has been disabled.
311
-     * @returns {void}
312
-     */
313
-    _tileViewToggled(enabled) {
314
-        this._local.tileViewEnabled = enabled;
315
-    }
316
-
317
-    /**
318
-     * Changes the nextOnStage property value.
319
-     *
320
-     * @param smallVideo the {SmallVideo} that was pinned or unpinned
321
-     * @param isPinned indicates if the given {SmallVideo} was pinned or
322
-     * unpinned
323
-     * @private
324
-     */
325
-    _nextOnStage(videoId, isPinned) {
326
-        if (!this._conference.isModerator) {
327
-            return;
328
-        }
329
-
330
-        let nextOnStage = null;
331
-
332
-        if (isPinned) {
333
-            nextOnStage = videoId;
334
-        }
335
-
336
-        this._local.nextOnStage = nextOnStage;
337
-    }
338
-
339
-    /**
340
-     * Sends the follow-me command, when a local property change occurs.
341
-     *
342
-     * @private
343
-     */
344
-    _localPropertyChange() { // eslint-disable-next-line no-unused-vars
345
-        // Only a moderator is allowed to send commands.
346
-        const conference = this._conference;
347
-
348
-        if (!conference.isModerator) {
349
-            return;
350
-        }
351
-
352
-        const commands = conference.commands;
353
-
354
-        // XXX The "Follow Me" command represents a snapshot of all states
355
-        // which are to be followed so don't forget to removeCommand before
356
-        // sendCommand!
357
-
358
-        commands.removeCommand(_COMMAND);
359
-        const local = this._local;
360
-
361
-        commands.sendCommandOnce(
362
-                _COMMAND,
363
-                {
364
-                    attributes: {
365
-                        filmstripVisible: local.filmstripVisible,
366
-                        nextOnStage: local.nextOnStage,
367
-                        sharedDocumentVisible: local.sharedDocumentVisible,
368
-                        tileViewEnabled: local.tileViewEnabled
369
-                    }
370
-                });
371
-    }
372
-
373
-    /**
374
-     * Notifies this instance about a &qout;Follow Me&qout; command (delivered
375
-     * by the Command(s) API of {this._conference}).
376
-     *
377
-     * @param attributes the attributes {Object} carried by the command
378
-     * @param id the identifier of the participant who issued the command. A
379
-     * notable idiosyncrasy of the Command(s) API to be mindful of here is that
380
-     * the command may be issued by the local participant.
381
-     */
382
-    _onFollowMeCommand({ attributes }, id) {
383
-        // We require to know who issued the command because (1) only a
384
-        // moderator is allowed to send commands and (2) a command MUST be
385
-        // issued by a defined commander.
386
-        if (typeof id === 'undefined') {
387
-            return;
388
-        }
389
-
390
-        // The Command(s) API will send us our own commands and we don't want
391
-        // to act upon them.
392
-        if (this._conference.isLocalId(id)) {
393
-            return;
394
-        }
395
-
396
-        if (!this._conference.isParticipantModerator(id)) {
397
-            logger.warn('Received follow-me command not from moderator');
398
-
399
-            return;
400
-        }
401
-
402
-        // Applies the received/remote command to the user experience/interface
403
-        // of the local participant.
404
-        this._onFilmstripVisible(attributes.filmstripVisible);
405
-        this._onNextOnStage(attributes.nextOnStage);
406
-        this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
407
-        this._onTileViewEnabled(attributes.tileViewEnabled);
408
-    }
409
-
410
-    /**
411
-     * Process a filmstrip open / close event received from FOLLOW-ME
412
-     * command.
413
-     * @param filmstripVisible indicates if the filmstrip has been shown or
414
-     * hidden
415
-     * @private
416
-     */
417
-    _onFilmstripVisible(filmstripVisible) {
418
-        if (typeof filmstripVisible !== 'undefined') {
419
-            // XXX The Command(s) API doesn't preserve the types (of
420
-            // attributes, at least) at the time of this writing so take into
421
-            // account that what originated as a Boolean may be a String on
422
-            // receipt.
423
-            // eslint-disable-next-line eqeqeq, no-param-reassign
424
-            filmstripVisible = filmstripVisible == 'true';
425
-
426
-            // FIXME The UI (module) very likely doesn't (want to) expose its
427
-            // eventEmitter as a public field. I'm not sure at the time of this
428
-            // writing whether calling UI.toggleFilmstrip() is acceptable (from
429
-            // a design standpoint) either.
430
-            if (filmstripVisible !== this._UI.isFilmstripVisible()) {
431
-                this._UI.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP);
432
-            }
433
-        }
434
-    }
435
-
436
-    /**
437
-     * Process the id received from a FOLLOW-ME command.
438
-     * @param id the identifier of the next participant to show on stage or
439
-     * undefined if we're clearing the stage (we're unpining all pined and we
440
-     * rely on dominant speaker events)
441
-     * @private
442
-     */
443
-    _onNextOnStage(id) {
444
-        let clickId = null;
445
-        let pin;
446
-
447
-        // if there is an id which is not pinned we schedule it for pin only the
448
-        // first time
449
-
450
-        if (typeof id !== 'undefined' && !VideoLayout.isPinned(id)) {
451
-            clickId = id;
452
-            pin = true;
453
-        } else if (typeof id === 'undefined' && VideoLayout.getPinnedId()) {
454
-            // if there is no id, but we have a pinned one, let's unpin
455
-            clickId = VideoLayout.getPinnedId();
456
-            pin = false;
457
-        }
458
-
459
-        if (clickId) {
460
-            this._pinVideoThumbnailById(clickId, pin);
461
-        }
462
-    }
463
-
464
-    /**
465
-     * Process a shared document open / close event received from FOLLOW-ME
466
-     * command.
467
-     * @param sharedDocumentVisible indicates if the shared document has been
468
-     * opened or closed
469
-     * @private
470
-     */
471
-    _onSharedDocumentVisible(sharedDocumentVisible) {
472
-        if (typeof sharedDocumentVisible !== 'undefined') {
473
-            // XXX The Command(s) API doesn't preserve the types (of
474
-            // attributes, at least) at the time of this writing so take into
475
-            // account that what originated as a Boolean may be a String on
476
-            // receipt.
477
-            // eslint-disable-next-line eqeqeq, no-param-reassign
478
-            sharedDocumentVisible = sharedDocumentVisible == 'true';
479
-
480
-            if (sharedDocumentVisible
481
-                !== this._UI.getSharedDocumentManager().isVisible()) {
482
-                this._UI.getSharedDocumentManager().toggleEtherpad();
483
-            }
484
-        }
485
-    }
486
-
487
-    /**
488
-     * Process a tile view enabled / disabled event received from FOLLOW-ME.
489
-     *
490
-     * @param {boolean} enabled - Whether or not tile view should be shown.
491
-     * @private
492
-     * @returns {void}
493
-     */
494
-    _onTileViewEnabled(enabled) {
495
-        if (typeof enabled === 'undefined') {
496
-            return;
497
-        }
498
-
499
-        APP.store.dispatch(setTileView(enabled === 'true'));
500
-    }
501
-
502
-    /**
503
-     * Pins / unpins the video thumbnail given by clickId.
504
-     *
505
-     * @param clickId the identifier of the video thumbnail to pin or unpin
506
-     * @param pin {true} to pin, {false} to unpin
507
-     * @private
508
-     */
509
-    _pinVideoThumbnailById(clickId, pin) {
510
-        const self = this;
511
-        const smallVideo = VideoLayout.getSmallVideo(clickId);
512
-
513
-        // If the SmallVideo for the given clickId exists we proceed with the
514
-        // pin/unpin.
515
-        if (smallVideo) {
516
-            this.nextOnStageTimer = 0;
517
-            clearTimeout(this.nextOnStageTimout);
518
-
519
-            if (pin) {
520
-                APP.store.dispatch(pinParticipant(clickId));
521
-            } else {
522
-                const { id } = getPinnedParticipant(APP.store.getState()) || {};
523
-
524
-                if (id === clickId) {
525
-                    APP.store.dispatch(pinParticipant(null));
526
-                }
527
-            }
528
-        } else {
529
-            // If there's no SmallVideo object for the given id, lets wait and
530
-            // see if it's going to be created in the next 30sec.
531
-            this.nextOnStageTimout = setTimeout(function() {
532
-                if (self.nextOnStageTimer > _FOLLOW_ME_RECEIVED_TIMEOUT) {
533
-                    self.nextOnStageTimer = 0;
534
-
535
-                    return;
536
-                }
537
-
538
-                // eslint-disable-next-line no-invalid-this
539
-                this.nextOnStageTimer++;
540
-                self._pinVideoThumbnailById(clickId, pin);
541
-            }, 1000);
542
-        }
543
-    }
544
-}
545
-
546
-export default FollowMe;

+ 0
- 12
modules/UI/UI.js View File

@@ -32,7 +32,6 @@ import {
32 32
 const EventEmitter = require('events');
33 33
 
34 34
 UI.messageHandler = messageHandler;
35
-import FollowMe from '../FollowMe';
36 35
 
37 36
 const eventEmitter = new EventEmitter();
38 37
 
@@ -41,8 +40,6 @@ UI.eventEmitter = eventEmitter;
41 40
 let etherpadManager;
42 41
 let sharedVideoManager;
43 42
 
44
-let followMeHandler;
45
-
46 43
 const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
47 44
     microphone: {},
48 45
     camera: {}
@@ -86,9 +83,6 @@ const UIListeners = new Map([
86 83
     ], [
87 84
         UIEvents.TOGGLE_FILMSTRIP,
88 85
         () => UI.toggleFilmstrip()
89
-    ], [
90
-        UIEvents.FOLLOW_ME_ENABLED,
91
-        enabled => followMeHandler && followMeHandler.enableFollowMe(enabled)
92 86
     ]
93 87
 ]);
94 88
 
@@ -193,12 +187,6 @@ UI.initConference = function() {
193 187
     if (displayName) {
194 188
         UI.changeDisplayName('localVideoContainer', displayName);
195 189
     }
196
-
197
-    // FollowMe attempts to copy certain aspects of the moderator's UI into the
198
-    // other participants' UI. Consequently, it needs (1) read and write access
199
-    // to the UI (depending on the moderator role of the local participant) and
200
-    // (2) APP.conference as means of communication between the participants.
201
-    followMeHandler = new FollowMe(APP.conference, UI);
202 190
 };
203 191
 
204 192
 /**

+ 0
- 4
modules/UI/etherpad/Etherpad.js View File

@@ -5,7 +5,6 @@ import { getToolboxHeight } from '../../../react/features/toolbox';
5 5
 
6 6
 import VideoLayout from '../videolayout/VideoLayout';
7 7
 import LargeContainer from '../videolayout/LargeContainer';
8
-import UIEvents from '../../../service/UI/UIEvents';
9 8
 import Filmstrip from '../videolayout/Filmstrip';
10 9
 
11 10
 /**
@@ -250,9 +249,6 @@ export default class EtherpadManager {
250 249
         VideoLayout.showLargeVideoContainer(
251 250
             ETHERPAD_CONTAINER_TYPE, !isVisible);
252 251
 
253
-        this.eventEmitter
254
-            .emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);
255
-
256 252
         APP.store.dispatch(setDocumentEditingState(!isVisible));
257 253
     }
258 254
 }

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

@@ -4,6 +4,7 @@ import React, { Fragment } from 'react';
4 4
 
5 5
 import { BaseApp } from '../../base/app';
6 6
 import { toURLString } from '../../base/util';
7
+import '../../follow-me';
7 8
 import { OverlayContainer } from '../../overlay';
8 9
 
9 10
 import { appNavigate } from '../actions';

+ 0
- 6
react/features/base/conference/actions.js View File

@@ -1,7 +1,5 @@
1 1
 // @flow
2 2
 
3
-import UIEvents from '../../../../service/UI/UIEvents';
4
-
5 3
 import {
6 4
     createStartMutedConfigurationEvent,
7 5
     sendAnalytics
@@ -547,10 +545,6 @@ export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
547 545
  * }}
548 546
  */
549 547
 export function setFollowMe(enabled: boolean) {
550
-    if (typeof APP !== 'undefined') {
551
-        APP.UI.emitEvent(UIEvents.FOLLOW_ME_ENABLED, enabled);
552
-    }
553
-
554 548
     return {
555 549
         type: SET_FOLLOW_ME,
556 550
         enabled

+ 39
- 18
react/features/etherpad/reducer.js View File

@@ -7,24 +7,45 @@ import {
7 7
     SET_DOCUMENT_EDITING_STATUS
8 8
 } from './actionTypes';
9 9
 
10
+const DEFAULT_STATE = {
11
+
12
+    /**
13
+     * Whether or not Etherpad is currently open.
14
+     *
15
+     * @public
16
+     * @type {boolean}
17
+     */
18
+    editing: false,
19
+
20
+    /**
21
+     * Whether or not Etherpad is ready to use.
22
+     *
23
+     * @public
24
+     * @type {boolean}
25
+     */
26
+    initialized: false
27
+};
28
+
10 29
 /**
11 30
  * Reduces the Redux actions of the feature features/etherpad.
12 31
  */
13
-ReducerRegistry.register('features/etherpad', (state = {}, action) => {
14
-    switch (action.type) {
15
-    case ETHERPAD_INITIALIZED:
16
-        return {
17
-            ...state,
18
-            initialized: true
19
-        };
20
-
21
-    case SET_DOCUMENT_EDITING_STATUS:
22
-        return {
23
-            ...state,
24
-            editing: action.editing
25
-        };
26
-
27
-    default:
28
-        return state;
29
-    }
30
-});
32
+ReducerRegistry.register(
33
+    'features/etherpad',
34
+    (state = DEFAULT_STATE, action) => {
35
+        switch (action.type) {
36
+        case ETHERPAD_INITIALIZED:
37
+            return {
38
+                ...state,
39
+                initialized: true
40
+            };
41
+
42
+        case SET_DOCUMENT_EDITING_STATUS:
43
+            return {
44
+                ...state,
45
+                editing: action.editing
46
+            };
47
+
48
+        default:
49
+            return state;
50
+        }
51
+    });

+ 6
- 0
react/features/follow-me/constants.js View File

@@ -0,0 +1,6 @@
1
+/**
2
+ * The (name of the) command which transports the state (represented by
3
+ * {State} for the local state at the time of this writing) of a {FollowMe}
4
+ * (instance) between participants.
5
+ */
6
+export const FOLLOW_ME_COMMAND = 'follow-me';

+ 2
- 0
react/features/follow-me/index.js View File

@@ -0,0 +1,2 @@
1
+export * from './middleware';
2
+export * from './subscriber';

+ 162
- 0
react/features/follow-me/middleware.js View File

@@ -0,0 +1,162 @@
1
+// @flow
2
+
3
+import { CONFERENCE_WILL_JOIN } from '../base/conference';
4
+import {
5
+    getParticipantById,
6
+    getPinnedParticipant,
7
+    pinParticipant
8
+} from '../base/participants';
9
+import { MiddlewareRegistry } from '../base/redux';
10
+import { setFilmstripVisible } from '../filmstrip';
11
+import { setTileView } from '../video-layout';
12
+
13
+import { FOLLOW_ME_COMMAND } from './constants';
14
+
15
+const logger = require('jitsi-meet-logger').getLogger(__filename);
16
+
17
+declare var APP: Object;
18
+
19
+/**
20
+ * The timeout after which a follow-me command that has been received will be
21
+ * ignored if not consumed.
22
+ *
23
+ * @type {number} in seconds
24
+ * @private
25
+ */
26
+const _FOLLOW_ME_RECEIVED_TIMEOUT = 30;
27
+
28
+/**
29
+ * An instance of a timeout used as a workaround when attempting to pin a
30
+ * non-existent particapant, which may be caused by participant join information
31
+ * not being received yet.
32
+ *
33
+ * @type {TimeoutID}
34
+ */
35
+let nextOnStageTimeout;
36
+
37
+/**
38
+ * A count of how many seconds the nextOnStageTimeout has ticked while waiting
39
+ * for a participant to be discovered that should be pinned. This variable
40
+ * works in conjunction with {@code _FOLLOW_ME_RECEIVED_TIMEOUT} and
41
+ * {@code nextOnStageTimeout}.
42
+ *
43
+ * @type {number}
44
+ */
45
+let nextOnStageTimer = 0;
46
+
47
+/**
48
+ * Represents "Follow Me" feature which enables a moderator to (partially)
49
+ * control the user experience/interface (e.g. filmstrip visibility) of (other)
50
+ * non-moderator participant.
51
+ */
52
+MiddlewareRegistry.register(store => next => action => {
53
+    switch (action.type) {
54
+    case CONFERENCE_WILL_JOIN: {
55
+        const { conference } = action;
56
+
57
+        conference.addCommandListener(
58
+            FOLLOW_ME_COMMAND, ({ attributes }, id) => {
59
+                _onFollowMeCommand(attributes, id, store);
60
+            });
61
+    }
62
+    }
63
+
64
+    return next(action);
65
+});
66
+
67
+/**
68
+ * Notifies this instance about a "Follow Me" command received by the Jitsi
69
+ * conference.
70
+ *
71
+ * @param {Object} attributes - The attributes carried by the command.
72
+ * @param {string} id - The identifier of the participant who issuing the
73
+ * command. A notable idiosyncrasy to be mindful of here is that the command
74
+ * may be issued by the local participant.
75
+ * @param {Object} store - The redux store. Used to calculate and dispatch
76
+ * updates.
77
+ * @private
78
+ * @returns {void}
79
+ */
80
+function _onFollowMeCommand(attributes = {}, id, store) {
81
+    const state = store.getState();
82
+
83
+    // We require to know who issued the command because (1) only a
84
+    // moderator is allowed to send commands and (2) a command MUST be
85
+    // issued by a defined commander.
86
+    if (typeof id === 'undefined') {
87
+        return;
88
+    }
89
+
90
+    const participantSendingCommand = getParticipantById(state, id);
91
+
92
+    // The Command(s) API will send us our own commands and we don't want
93
+    // to act upon them.
94
+    if (participantSendingCommand.local) {
95
+        return;
96
+    }
97
+
98
+    if (participantSendingCommand.role !== 'moderator') {
99
+        logger.warn('Received follow-me command not from moderator');
100
+
101
+        return;
102
+    }
103
+
104
+    // XMPP will translate all booleans to strings, so explicitly check against
105
+    // the string form of the boolean {@code true}.
106
+    store.dispatch(setFilmstripVisible(attributes.filmstripVisible === 'true'));
107
+    store.dispatch(setTileView(attributes.tileViewEnabled === 'true'));
108
+
109
+    // For now gate etherpad checks behind a web-app check to be extra safe
110
+    // against calling a web-app global.
111
+    if (typeof APP !== 'undefined' && state['features/etherpad'].initialized) {
112
+        const isEtherpadVisible = attributes.sharedDocumentVisible === 'true';
113
+        const documentManager = APP.UI.getSharedDocumentManager();
114
+
115
+        if (documentManager
116
+                && isEtherpadVisible !== state['features/etherpad'].editing) {
117
+            documentManager.toggleEtherpad();
118
+        }
119
+    }
120
+
121
+    const pinnedParticipant
122
+        = getPinnedParticipant(state, attributes.nextOnStage);
123
+    const idOfParticipantToPin = attributes.nextOnStage;
124
+
125
+    if (typeof idOfParticipantToPin !== 'undefined'
126
+            && (!pinnedParticipant
127
+                || idOfParticipantToPin !== pinnedParticipant.id)) {
128
+        _pinVideoThumbnailById(store, idOfParticipantToPin);
129
+    } else if (typeof idOfParticipantToPin === 'undefined'
130
+            && pinnedParticipant) {
131
+        store.dispatch(pinParticipant(null));
132
+    }
133
+}
134
+
135
+/**
136
+ * Pins the video thumbnail given by clickId.
137
+ *
138
+ * @param {Object} store - The redux store.
139
+ * @param {string} clickId - The identifier of the participant to pin.
140
+ * @private
141
+ * @returns {void}
142
+ */
143
+function _pinVideoThumbnailById(store, clickId) {
144
+    if (getParticipantById(store.getState(), clickId)) {
145
+        clearTimeout(nextOnStageTimeout);
146
+        nextOnStageTimer = 0;
147
+
148
+        store.dispatch(pinParticipant(clickId));
149
+    } else {
150
+        nextOnStageTimeout = setTimeout(() => {
151
+            if (nextOnStageTimer > _FOLLOW_ME_RECEIVED_TIMEOUT) {
152
+                nextOnStageTimer = 0;
153
+
154
+                return;
155
+            }
156
+
157
+            nextOnStageTimer++;
158
+
159
+            _pinVideoThumbnailById(store, clickId);
160
+        }, 1000);
161
+    }
162
+}

+ 110
- 0
react/features/follow-me/subscriber.js View File

@@ -0,0 +1,110 @@
1
+// @flow
2
+
3
+import { StateListenerRegistry } from '../base/redux';
4
+import { getCurrentConference } from '../base/conference';
5
+import {
6
+    getPinnedParticipant,
7
+    isLocalParticipantModerator
8
+} from '../base/participants';
9
+
10
+import { FOLLOW_ME_COMMAND } from './constants';
11
+
12
+/**
13
+ * Subscribes to changes to the Follow Me setting for the local participant to
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
18
+ */
19
+StateListenerRegistry.register(
20
+    /* selector */ state => state['features/base/conference'].followMeEnabled,
21
+    /* listener */ _sendFollowMeCommand);
22
+
23
+/**
24
+ * Subscribes to changes to the currently pinned participant in the user
25
+ * interface of the local participant.
26
+ */
27
+StateListenerRegistry.register(
28
+    /* selector */ state => {
29
+        const pinnedParticipant = getPinnedParticipant(state);
30
+
31
+        return pinnedParticipant ? pinnedParticipant.id : null;
32
+    },
33
+    /* listener */ _sendFollowMeCommand);
34
+
35
+/**
36
+ * Subscribes to changes to the shared document (etherpad) visibility in the
37
+ * user interface of the local participant.
38
+ *
39
+ * @param sharedDocumentVisible {Boolean} {true} if the shared document was
40
+ * shown (as a result of the toggle) or {false} if it was hidden
41
+ */
42
+StateListenerRegistry.register(
43
+    /* selector */ state => state['features/etherpad'].editing,
44
+    /* listener */ _sendFollowMeCommand);
45
+
46
+/**
47
+ * Subscribes to changes to the filmstrip visibility in the user interface of
48
+ * the local participant.
49
+ */
50
+StateListenerRegistry.register(
51
+    /* selector */ state => state['features/filmstrip'].visible,
52
+    /* listener */ _sendFollowMeCommand);
53
+
54
+/**
55
+ * Subscribes to changes to the tile view setting in the user interface of the
56
+ * local participant.
57
+ */
58
+StateListenerRegistry.register(
59
+    /* selector */ state => state['features/video-layout'].tileViewEnabled,
60
+    /* listener */ _sendFollowMeCommand);
61
+
62
+/**
63
+ * Private selector for returning state from redux that should be respected by
64
+ * other participants while follow me is enabled.
65
+ *
66
+ * @param {Object} state - The redux state.
67
+ * @returns {Object}
68
+ */
69
+function _getFollowMeState(state) {
70
+    const pinnedParticipant = getPinnedParticipant(state);
71
+
72
+    return {
73
+        filmstripVisible: state['features/filmstrip'].visible,
74
+        nextOnStage: pinnedParticipant && pinnedParticipant.id,
75
+        sharedDocumentVisible: state['features/etherpad'].editing,
76
+        tileViewEnabled: state['features/video-layout'].tileViewEnabled
77
+    };
78
+}
79
+
80
+/**
81
+ * Sends the follow-me command, when a local property change occurs.
82
+ *
83
+ * @param {*} newSelectedValue - The changed selected value from the selector.
84
+ * @param {Object} store - The redux store.
85
+ * @private
86
+ * @returns {void}
87
+ */
88
+function _sendFollowMeCommand(
89
+        newSelectedValue, store) { // eslint-disable-line no-unused-vars
90
+    const state = store.getState();
91
+    const conference = getCurrentConference(state);
92
+
93
+    if (!conference || !state['features/base/conference'].followMeEnabled) {
94
+        return;
95
+    }
96
+
97
+    // Only a moderator is allowed to send commands.
98
+    if (!isLocalParticipantModerator(state)) {
99
+        return;
100
+    }
101
+
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(
107
+        FOLLOW_ME_COMMAND,
108
+        { attributes: _getFollowMeState(state) }
109
+    );
110
+}

+ 0
- 11
react/features/video-layout/middleware.web.js View File

@@ -1,7 +1,6 @@
1 1
 // @flow
2 2
 
3 3
 import VideoLayout from '../../../modules/UI/videolayout/VideoLayout.js';
4
-import UIEvents from '../../../service/UI/UIEvents';
5 4
 
6 5
 import { CONFERENCE_JOINED, CONFERENCE_WILL_LEAVE } from '../base/conference';
7 6
 import {
@@ -16,7 +15,6 @@ import { MiddlewareRegistry } from '../base/redux';
16 15
 import { TRACK_ADDED } from '../base/tracks';
17 16
 import { SET_FILMSTRIP_VISIBLE } from '../filmstrip';
18 17
 
19
-import { SET_TILE_VIEW } from './actionTypes';
20 18
 import './middleware.any';
21 19
 
22 20
 declare var APP: Object;
@@ -73,22 +71,13 @@ MiddlewareRegistry.register(store => next => action => {
73 71
 
74 72
     case PIN_PARTICIPANT:
75 73
         VideoLayout.onPinChange(action.participant.id);
76
-        APP.UI.emitEvent(
77
-            UIEvents.PINNED_ENDPOINT,
78
-            action.participant.id,
79
-            Boolean(action.participant.id));
80 74
         break;
81 75
 
82 76
     case SET_FILMSTRIP_VISIBLE:
83 77
         VideoLayout.resizeVideoArea(true, false);
84
-        APP.UI.emitEvent(UIEvents.TOGGLED_FILMSTRIP, action.visible);
85 78
         APP.API.notifyFilmstripDisplayChanged(action.visible);
86 79
         break;
87 80
 
88
-    case SET_TILE_VIEW:
89
-        APP.UI.emitEvent(UIEvents.TOGGLED_TILE_VIEW, action.enabled);
90
-        break;
91
-
92 81
     case TRACK_ADDED:
93 82
         if (!action.track.local) {
94 83
             VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);

+ 12
- 0
react/features/video-layout/reducer.js View File

@@ -12,6 +12,18 @@ const DEFAULT_STATE = {
12 12
     screenShares: []
13 13
 };
14 14
 
15
+const DEFAULT_STATE = {
16
+
17
+    /**
18
+     * The indicator which determines whether the video layout should display
19
+     * video thumbnails in a tiled layout.
20
+     *
21
+     * @public
22
+     * @type {boolean}
23
+     */
24
+    tileViewEnabled: false
25
+};
26
+
15 27
 const STORE_NAME = 'features/video-layout';
16 28
 
17 29
 PersistenceRegistry.register(STORE_NAME, {

+ 0
- 17
service/UI/UIEvents.js View File

@@ -1,6 +1,5 @@
1 1
 export default {
2 2
     NICKNAME_CHANGED: 'UI.nickname_changed',
3
-    PINNED_ENDPOINT: 'UI.pinned_endpoint',
4 3
 
5 4
     /**
6 5
      * Notifies that local user changed email.
@@ -43,28 +42,12 @@ export default {
43 42
      */
44 43
     TOGGLE_FILMSTRIP: 'UI.toggle_filmstrip',
45 44
 
46
-    /**
47
-     * Notifies that the filmstrip was (actually) toggled. The event supplies a
48
-     * {Boolean} (primitive) value indicating the visibility of the filmstrip
49
-     * after the toggling (at the time of the event emission).
50
-     *
51
-     * @see {TOGGLE_FILMSTRIP}
52
-     */
53
-    TOGGLED_FILMSTRIP: 'UI.toggled_filmstrip',
54 45
     TOGGLE_SCREENSHARING: 'UI.toggle_screensharing',
55
-    TOGGLED_SHARED_DOCUMENT: 'UI.toggled_shared_document',
56
-    TOGGLED_TILE_VIEW: 'UI.toggled_tile_view',
57 46
     HANGUP: 'UI.hangup',
58 47
     LOGOUT: 'UI.logout',
59 48
     VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
60 49
     AUDIO_DEVICE_CHANGED: 'UI.audio_device_changed',
61 50
 
62
-    /**
63
-     * Notifies interested listeners that the follow-me feature is enabled or
64
-     * disabled.
65
-     */
66
-    FOLLOW_ME_ENABLED: 'UI.follow_me_enabled',
67
-
68 51
     /**
69 52
      * Notifies that flipX property of the local video is changed.
70 53
      */

Loading…
Cancel
Save