| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 | 
							- // @flow
 - 
 - import {
 -     setFollowMeModerator,
 -     setFollowMeState
 - } from './actions';
 - import { CONFERENCE_WILL_JOIN } from '../base/conference';
 - import {
 -     getParticipantById,
 -     getPinnedParticipant,
 -     PARTICIPANT_LEFT,
 -     pinParticipant
 - } from '../base/participants';
 - import { MiddlewareRegistry } from '../base/redux';
 - import { setFilmstripVisible } from '../filmstrip';
 - import { setTileView } from '../video-layout';
 - 
 - import { FOLLOW_ME_COMMAND } from './constants';
 - 
 - const logger = require('jitsi-meet-logger').getLogger(__filename);
 - 
 - declare var APP: Object;
 - 
 - /**
 -  * The timeout after which a follow-me command that has been received will be
 -  * ignored if not consumed.
 -  *
 -  * @type {number} in seconds
 -  * @private
 -  */
 - const _FOLLOW_ME_RECEIVED_TIMEOUT = 30;
 - 
 - /**
 -  * An instance of a timeout used as a workaround when attempting to pin a
 -  * non-existent particapant, which may be caused by participant join information
 -  * not being received yet.
 -  *
 -  * @type {TimeoutID}
 -  */
 - let nextOnStageTimeout;
 - 
 - /**
 -  * A count of how many seconds the nextOnStageTimeout has ticked while waiting
 -  * for a participant to be discovered that should be pinned. This variable
 -  * works in conjunction with {@code _FOLLOW_ME_RECEIVED_TIMEOUT} and
 -  * {@code nextOnStageTimeout}.
 -  *
 -  * @type {number}
 -  */
 - let nextOnStageTimer = 0;
 - 
 - /**
 -  * Represents "Follow Me" feature which enables a moderator to (partially)
 -  * control the user experience/interface (e.g. filmstrip visibility) of (other)
 -  * non-moderator participant.
 -  */
 - MiddlewareRegistry.register(store => next => action => {
 -     switch (action.type) {
 -     case CONFERENCE_WILL_JOIN: {
 -         const { conference } = action;
 - 
 -         conference.addCommandListener(
 -             FOLLOW_ME_COMMAND, ({ attributes }, id) => {
 -                 _onFollowMeCommand(attributes, id, store);
 -             });
 -         break;
 -     }
 -     case PARTICIPANT_LEFT:
 -         if (store.getState()['features/follow-me'].moderator === action.participant.id) {
 -             store.dispatch(setFollowMeModerator());
 -         }
 -         break;
 -     }
 - 
 -     return next(action);
 - });
 - 
 - /**
 -  * Notifies this instance about a "Follow Me" command received by the Jitsi
 -  * conference.
 -  *
 -  * @param {Object} attributes - The attributes carried by the command.
 -  * @param {string} id - The identifier of the participant who issuing the
 -  * command. A notable idiosyncrasy to be mindful of here is that the command
 -  * may be issued by the local participant.
 -  * @param {Object} store - The redux store. Used to calculate and dispatch
 -  * updates.
 -  * @private
 -  * @returns {void}
 -  */
 - function _onFollowMeCommand(attributes = {}, id, store) {
 -     const state = store.getState();
 - 
 -     // We require to know who issued the command because (1) only a
 -     // moderator is allowed to send commands and (2) a command MUST be
 -     // issued by a defined commander.
 -     if (typeof id === 'undefined') {
 -         return;
 -     }
 - 
 -     const participantSendingCommand = getParticipantById(state, id);
 - 
 -     // The Command(s) API will send us our own commands and we don't want
 -     // to act upon them.
 -     if (participantSendingCommand.local) {
 -         return;
 -     }
 - 
 -     if (participantSendingCommand.role !== 'moderator') {
 -         logger.warn('Received follow-me command not from moderator');
 - 
 -         return;
 -     }
 - 
 -     if (!state['features/follow-me'].moderator) {
 -         store.dispatch(setFollowMeModerator(id));
 -     }
 - 
 -     // just a command that follow me was turned off
 -     if (attributes.off) {
 -         store.dispatch(setFollowMeModerator());
 - 
 -         return;
 -     }
 - 
 -     const oldState = state['features/follow-me'].state || {};
 - 
 -     store.dispatch(setFollowMeState(attributes));
 - 
 -     // XMPP will translate all booleans to strings, so explicitly check against
 -     // the string form of the boolean {@code true}.
 -     if (oldState.filmstripVisible !== attributes.filmstripVisible) {
 -         store.dispatch(setFilmstripVisible(attributes.filmstripVisible === 'true'));
 -     }
 - 
 -     if (oldState.tileViewEnabled !== attributes.tileViewEnabled) {
 -         store.dispatch(setTileView(attributes.tileViewEnabled === 'true'));
 -     }
 - 
 -     // For now gate etherpad checks behind a web-app check to be extra safe
 -     // against calling a web-app global.
 -     if (typeof APP !== 'undefined'
 -         && state['features/etherpad'].initialized
 -         && oldState.sharedDocumentVisible !== attributes.sharedDocumentVisible) {
 -         const isEtherpadVisible = attributes.sharedDocumentVisible === 'true';
 -         const documentManager = APP.UI.getSharedDocumentManager();
 - 
 -         if (documentManager
 -                 && isEtherpadVisible !== state['features/etherpad'].editing) {
 -             documentManager.toggleEtherpad();
 -         }
 -     }
 - 
 -     const pinnedParticipant
 -         = getPinnedParticipant(state, attributes.nextOnStage);
 -     const idOfParticipantToPin = attributes.nextOnStage;
 - 
 -     if (typeof idOfParticipantToPin !== 'undefined'
 -             && (!pinnedParticipant
 -                 || idOfParticipantToPin !== pinnedParticipant.id)
 -             && oldState.nextOnStage !== attributes.nextOnStage) {
 -         _pinVideoThumbnailById(store, idOfParticipantToPin);
 -     } else if (typeof idOfParticipantToPin === 'undefined'
 -             && pinnedParticipant) {
 -         store.dispatch(pinParticipant(null));
 -     }
 - }
 - 
 - /**
 -  * Pins the video thumbnail given by clickId.
 -  *
 -  * @param {Object} store - The redux store.
 -  * @param {string} clickId - The identifier of the participant to pin.
 -  * @private
 -  * @returns {void}
 -  */
 - function _pinVideoThumbnailById(store, clickId) {
 -     if (getParticipantById(store.getState(), clickId)) {
 -         clearTimeout(nextOnStageTimeout);
 -         nextOnStageTimer = 0;
 - 
 -         store.dispatch(pinParticipant(clickId));
 -     } else {
 -         nextOnStageTimeout = setTimeout(() => {
 -             if (nextOnStageTimer > _FOLLOW_ME_RECEIVED_TIMEOUT) {
 -                 nextOnStageTimer = 0;
 - 
 -                 return;
 -             }
 - 
 -             nextOnStageTimer++;
 - 
 -             _pinVideoThumbnailById(store, clickId);
 -         }, 1000);
 -     }
 - }
 
 
  |