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.
pull/4147/head jitsi-meet_3702
Дамян Минков 6 years ago committed by GitHub
parent 98c8fb09c4
commit a6555c5d24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      react/features/base/app/components/BaseApp.js
  2. 23
      react/features/follow-me/actionTypes.js
  3. 38
      react/features/follow-me/actions.js
  4. 2
      react/features/follow-me/index.js
  5. 42
      react/features/follow-me/middleware.js
  6. 33
      react/features/follow-me/reducer.js
  7. 29
      react/features/follow-me/subscriber.js
  8. 9
      react/features/settings/components/web/MoreTab.js
  9. 11
      react/features/settings/components/web/SettingsDialog.js
  10. 2
      react/features/settings/functions.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);
});
}
/**

@ -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';

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

@ -1,2 +1,4 @@
export * from './middleware';
export * from './subscriber';
import './reducer';

@ -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) {

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

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

@ -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<Props, State> {
*/
_renderModeratorSettings() {
const {
followMeActive,
followMeEnabled,
startAudioMuted,
startVideoMuted,
@ -221,7 +227,8 @@ class MoreTab extends AbstractDialogTab<Props, State> {
super._onChange({ startVideoMuted: checked })
} />
<Checkbox
isChecked = { followMeEnabled }
isChecked = { followMeEnabled && !followMeActive }
isDisabled = { followMeActive }
label = { t('settings.followMe') }
name = 'follow-me'
// eslint-disable-next-line react/jsx-no-bind

@ -190,6 +190,17 @@ function _mapStateToProps(state) {
component: MoreTab,
label: 'settings.more',
props: moreTabProps,
propsUpdateFunction: (tabState, newProps) => {
// 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
});

@ -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'),

Loading…
Cancel
Save