Skip to content

Commit

Permalink
ref(follow-me): hook into redux (jitsi#3991)
Browse files Browse the repository at this point in the history
Use subscribers to detect state change and emit those
out to other participants. Use middleware to register
the command listener.
  • Loading branch information
virtuacoplenny authored Apr 17, 2019
1 parent aefa836 commit c7013f5
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 614 deletions.
546 changes: 0 additions & 546 deletions modules/FollowMe.js

This file was deleted.

12 changes: 0 additions & 12 deletions modules/UI/UI.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {
const EventEmitter = require('events');

UI.messageHandler = messageHandler;
import FollowMe from '../FollowMe';

const eventEmitter = new EventEmitter();

Expand All @@ -41,8 +40,6 @@ UI.eventEmitter = eventEmitter;
let etherpadManager;
let sharedVideoManager;

let followMeHandler;

const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
microphone: {},
camera: {}
Expand Down Expand Up @@ -86,9 +83,6 @@ const UIListeners = new Map([
], [
UIEvents.TOGGLE_FILMSTRIP,
() => UI.toggleFilmstrip()
], [
UIEvents.FOLLOW_ME_ENABLED,
enabled => followMeHandler && followMeHandler.enableFollowMe(enabled)
]
]);

Expand Down Expand Up @@ -193,12 +187,6 @@ UI.initConference = function() {
if (displayName) {
UI.changeDisplayName('localVideoContainer', displayName);
}

// FollowMe attempts to copy certain aspects of the moderator's UI into the
// other participants' UI. Consequently, it needs (1) read and write access
// to the UI (depending on the moderator role of the local participant) and
// (2) APP.conference as means of communication between the participants.
followMeHandler = new FollowMe(APP.conference, UI);
};

/**
Expand Down
4 changes: 0 additions & 4 deletions modules/UI/etherpad/Etherpad.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { getToolboxHeight } from '../../../react/features/toolbox';

import VideoLayout from '../videolayout/VideoLayout';
import LargeContainer from '../videolayout/LargeContainer';
import UIEvents from '../../../service/UI/UIEvents';
import Filmstrip from '../videolayout/Filmstrip';

/**
Expand Down Expand Up @@ -250,9 +249,6 @@ export default class EtherpadManager {
VideoLayout.showLargeVideoContainer(
ETHERPAD_CONTAINER_TYPE, !isVisible);

this.eventEmitter
.emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);

APP.store.dispatch(setDocumentEditingState(!isVisible));
}
}
1 change: 1 addition & 0 deletions react/features/app/components/AbstractApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { Fragment } from 'react';

import { BaseApp } from '../../base/app';
import { toURLString } from '../../base/util';
import '../../follow-me';
import { OverlayContainer } from '../../overlay';

import { appNavigate } from '../actions';
Expand Down
6 changes: 0 additions & 6 deletions react/features/base/conference/actions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// @flow

import UIEvents from '../../../../service/UI/UIEvents';

import {
createStartMutedConfigurationEvent,
sendAnalytics
Expand Down Expand Up @@ -547,10 +545,6 @@ export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
* }}
*/
export function setFollowMe(enabled: boolean) {
if (typeof APP !== 'undefined') {
APP.UI.emitEvent(UIEvents.FOLLOW_ME_ENABLED, enabled);
}

return {
type: SET_FOLLOW_ME,
enabled
Expand Down
57 changes: 39 additions & 18 deletions react/features/etherpad/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,45 @@ import {
SET_DOCUMENT_EDITING_STATUS
} from './actionTypes';

const DEFAULT_STATE = {

/**
* Whether or not Etherpad is currently open.
*
* @public
* @type {boolean}
*/
editing: false,

/**
* Whether or not Etherpad is ready to use.
*
* @public
* @type {boolean}
*/
initialized: false
};

/**
* Reduces the Redux actions of the feature features/etherpad.
*/
ReducerRegistry.register('features/etherpad', (state = {}, action) => {
switch (action.type) {
case ETHERPAD_INITIALIZED:
return {
...state,
initialized: true
};

case SET_DOCUMENT_EDITING_STATUS:
return {
...state,
editing: action.editing
};

default:
return state;
}
});
ReducerRegistry.register(
'features/etherpad',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case ETHERPAD_INITIALIZED:
return {
...state,
initialized: true
};

case SET_DOCUMENT_EDITING_STATUS:
return {
...state,
editing: action.editing
};

default:
return state;
}
});
6 changes: 6 additions & 0 deletions react/features/follow-me/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* The (name of the) command which transports the state (represented by
* {State} for the local state at the time of this writing) of a {FollowMe}
* (instance) between participants.
*/
export const FOLLOW_ME_COMMAND = 'follow-me';
2 changes: 2 additions & 0 deletions react/features/follow-me/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './middleware';
export * from './subscriber';
162 changes: 162 additions & 0 deletions react/features/follow-me/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// @flow

import { CONFERENCE_WILL_JOIN } from '../base/conference';
import {
getParticipantById,
getPinnedParticipant,
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);
});
}
}

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;
}

// XMPP will translate all booleans to strings, so explicitly check against
// the string form of the boolean {@code true}.
store.dispatch(setFilmstripVisible(attributes.filmstripVisible === 'true'));
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) {
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)) {
_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);
}
}
Loading

0 comments on commit c7013f5

Please sign in to comment.