From a6555c5d244ed04ccd998ca962ff7d3a485abe8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Fri, 26 Apr 2019 18:11:53 +0000 Subject: [PATCH] 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. --- react/features/base/app/components/BaseApp.js | 12 +++++- react/features/follow-me/actionTypes.js | 23 ++++++++++ react/features/follow-me/actions.js | 38 +++++++++++++++++ react/features/follow-me/index.js | 2 + react/features/follow-me/middleware.js | 42 +++++++++++++++++-- react/features/follow-me/reducer.js | 33 +++++++++++++++ react/features/follow-me/subscriber.js | 29 ++++++++----- .../settings/components/web/MoreTab.js | 9 +++- .../settings/components/web/SettingsDialog.js | 11 +++++ react/features/settings/functions.js | 2 + 10 files changed, 184 insertions(+), 17 deletions(-) create mode 100644 react/features/follow-me/actionTypes.js create mode 100644 react/features/follow-me/actions.js create mode 100644 react/features/follow-me/reducer.js diff --git a/react/features/base/app/components/BaseApp.js b/react/features/base/app/components/BaseApp.js index 740a6c3d39..fef90f7eba 100644 --- a/react/features/base/app/components/BaseApp.js +++ b/react/features/base/app/components/BaseApp.js @@ -18,6 +18,8 @@ import { PersistenceRegistry } from '../../storage'; import { appWillMount, appWillUnmount } from '../actions'; +const logger = require('jitsi-meet-logger').getLogger(__filename); + declare var APP: Object; /** @@ -74,14 +76,20 @@ export default class BaseApp extends Component<*, State> { * @type {Promise} */ this._init = this._initStorage() - .catch(() => { /* BaseApp should always initialize! */ }) + .catch(err => { + /* BaseApp should always initialize! */ + logger.error(err); + }) .then(() => new Promise(resolve => { this.setState({ store: this._createStore() }, resolve); })) .then(() => this.state.store.dispatch(appWillMount(this))) - .catch(() => { /* BaseApp should always initialize! */ }); + .catch(err => { + /* BaseApp should always initialize! */ + logger.error(err); + }); } /** diff --git a/react/features/follow-me/actionTypes.js b/react/features/follow-me/actionTypes.js new file mode 100644 index 0000000000..5711fa6ab5 --- /dev/null +++ b/react/features/follow-me/actionTypes.js @@ -0,0 +1,23 @@ +// @flow + +/** + * The id of the Follow Me moderator. + * + * { + * type: SET_FOLLOW_ME_MODERATOR, + * id: boolean + * } + */ +export const SET_FOLLOW_ME_MODERATOR = 'SET_FOLLOW_ME_MODERATOR'; + +/** + * The type of (redux) action which updates the current known state of the + * Follow Me feature. + * + * + * { + * type: SET_FOLLOW_ME_STATE, + * state: boolean + * } + */ +export const SET_FOLLOW_ME_STATE = 'SET_FOLLOW_ME_STATE'; diff --git a/react/features/follow-me/actions.js b/react/features/follow-me/actions.js new file mode 100644 index 0000000000..ac64cbdb89 --- /dev/null +++ b/react/features/follow-me/actions.js @@ -0,0 +1,38 @@ +// @flow + +import { + SET_FOLLOW_ME_MODERATOR, + SET_FOLLOW_ME_STATE +} from './actionTypes'; + +/** + * Sets the current moderator id or clears it. + * + * @param {?string} id - The Follow Me moderator participant id. + * @returns {{ + * type: SET_FOLLOW_ME_MODERATOR, + * id, string + * }} + */ +export function setFollowMeModerator(id: ?string) { + return { + type: SET_FOLLOW_ME_MODERATOR, + id + }; +} + +/** + * Sets the Follow Me feature state. + * + * @param {?Object} state - The current state. + * @returns {{ + * type: SET_FOLLOW_ME_STATE, + * state: Object + * }} + */ +export function setFollowMeState(state: ?Object) { + return { + type: SET_FOLLOW_ME_STATE, + state + }; +} diff --git a/react/features/follow-me/index.js b/react/features/follow-me/index.js index fc9514bb29..fc45b694e4 100644 --- a/react/features/follow-me/index.js +++ b/react/features/follow-me/index.js @@ -1,2 +1,4 @@ export * from './middleware'; export * from './subscriber'; + +import './reducer'; diff --git a/react/features/follow-me/middleware.js b/react/features/follow-me/middleware.js index 0ad0ede601..582d07e2c4 100644 --- a/react/features/follow-me/middleware.js +++ b/react/features/follow-me/middleware.js @@ -1,9 +1,14 @@ // @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'; @@ -58,7 +63,13 @@ MiddlewareRegistry.register(store => next => action => { 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); @@ -101,14 +112,36 @@ function _onFollowMeCommand(attributes = {}, id, store) { 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}. - store.dispatch(setFilmstripVisible(attributes.filmstripVisible === 'true')); - store.dispatch(setTileView(attributes.tileViewEnabled === '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) { + if (typeof APP !== 'undefined' + && state['features/etherpad'].initialized + && oldState.sharedDocumentVisible !== attributes.sharedDocumentVisible) { const isEtherpadVisible = attributes.sharedDocumentVisible === 'true'; const documentManager = APP.UI.getSharedDocumentManager(); @@ -124,7 +157,8 @@ function _onFollowMeCommand(attributes = {}, id, store) { if (typeof idOfParticipantToPin !== 'undefined' && (!pinnedParticipant - || idOfParticipantToPin !== pinnedParticipant.id)) { + || idOfParticipantToPin !== pinnedParticipant.id) + && oldState.nextOnStage !== attributes.nextOnStage) { _pinVideoThumbnailById(store, idOfParticipantToPin); } else if (typeof idOfParticipantToPin === 'undefined' && pinnedParticipant) { diff --git a/react/features/follow-me/reducer.js b/react/features/follow-me/reducer.js new file mode 100644 index 0000000000..296703bb2e --- /dev/null +++ b/react/features/follow-me/reducer.js @@ -0,0 +1,33 @@ +// @flow + +import { + SET_FOLLOW_ME_MODERATOR, + SET_FOLLOW_ME_STATE +} from './actionTypes'; +import { ReducerRegistry, set } from '../base/redux'; + +/** + * Listen for actions that contain the Follow Me feature active state, so that it can be stored. + */ +ReducerRegistry.register( + 'features/follow-me', + (state = {}, action) => { + switch (action.type) { + + case SET_FOLLOW_ME_MODERATOR: { + let newState = set(state, 'moderator', action.id); + + if (!action.id) { + // clear the state if feature becomes disabled + newState = set(newState, 'state', undefined); + } + + return newState; + } + case SET_FOLLOW_ME_STATE: { + return set(state, 'state', action.state); + } + } + + return state; + }); diff --git a/react/features/follow-me/subscriber.js b/react/features/follow-me/subscriber.js index b702bffc30..31f6f82ee1 100644 --- a/react/features/follow-me/subscriber.js +++ b/react/features/follow-me/subscriber.js @@ -12,13 +12,12 @@ import { FOLLOW_ME_COMMAND } from './constants'; /** * Subscribes to changes to the Follow Me setting for the local participant to * notify remote participants of current user interface status. - * - * @param sharedDocumentVisible {Boolean} {true} if the shared document was - * shown (as a result of the toggle) or {false} if it was hidden + * Changing newSelectedValue param to off, when feature is turned of so we can + * notify all listeners. */ StateListenerRegistry.register( /* selector */ state => state['features/base/conference'].followMeEnabled, - /* listener */ _sendFollowMeCommand); + /* listener */ (newSelectedValue, store) => _sendFollowMeCommand(newSelectedValue || 'off', store)); /** * Subscribes to changes to the currently pinned participant in the user @@ -90,7 +89,7 @@ function _sendFollowMeCommand( const state = store.getState(); const conference = getCurrentConference(state); - if (!conference || !state['features/base/conference'].followMeEnabled) { + if (!conference) { return; } @@ -99,11 +98,21 @@ function _sendFollowMeCommand( return; } - // XXX The "Follow Me" command represents a snapshot of all states - // which are to be followed so don't forget to removeCommand before - // sendCommand! - conference.removeCommand(FOLLOW_ME_COMMAND); - conference.sendCommandOnce( + if (newSelectedValue === 'off') { + // if the change is to off, local user turned off follow me and + // we want to signal this + + conference.sendCommandOnce( + FOLLOW_ME_COMMAND, + { attributes: { off: true } } + ); + + return; + } else if (!state['features/base/conference'].followMeEnabled) { + return; + } + + conference.sendCommand( FOLLOW_ME_COMMAND, { attributes: _getFollowMeState(state) } ); diff --git a/react/features/settings/components/web/MoreTab.js b/react/features/settings/components/web/MoreTab.js index 47b479e2a3..1737141567 100644 --- a/react/features/settings/components/web/MoreTab.js +++ b/react/features/settings/components/web/MoreTab.js @@ -23,6 +23,11 @@ export type Props = { */ currentLanguage: string, + /** + * Whether or not follow me is currently active (enabled by some other participant). + */ + followMeActive: boolean, + /** * Whether or not the user has selected the Follow Me feature to be enabled. */ @@ -189,6 +194,7 @@ class MoreTab extends AbstractDialogTab { */ _renderModeratorSettings() { const { + followMeActive, followMeEnabled, startAudioMuted, startVideoMuted, @@ -221,7 +227,8 @@ class MoreTab extends AbstractDialogTab { super._onChange({ startVideoMuted: checked }) } /> { + // Updates tab props, keeping users selection + + return { + ...newProps, + currentLanguage: tabState.currentLanguage, + followMeEnabled: tabState.followMeEnabled, + startAudioMuted: tabState.startAudioMuted, + startVideoMuted: tabState.startVideoMuted + }; + }, styles: 'settings-pane more-pane', submit: submitMoreTab }); diff --git a/react/features/settings/functions.js b/react/features/settings/functions.js index 7ab059a761..e0d0134f26 100644 --- a/react/features/settings/functions.js +++ b/react/features/settings/functions.js @@ -81,6 +81,7 @@ export function getMoreTabProps(stateful: Object | Function) { startAudioMutedPolicy, startVideoMutedPolicy } = state['features/base/conference']; + const followMeActive = Boolean(state['features/follow-me'].moderator); const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || []; const localParticipant = getLocalParticipant(state); @@ -93,6 +94,7 @@ export function getMoreTabProps(stateful: Object | Function) { return { currentLanguage: language, + followMeActive: Boolean(conference && followMeActive), followMeEnabled: Boolean(conference && followMeEnabled), languages: LANGUAGES, showLanguageSettings: configuredTabs.includes('language'),