diff --git a/react/features/base/app/components/BaseApp.js b/react/features/base/app/components/BaseApp.js index 740a6c3d3..fef90f7eb 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 000000000..5711fa6ab --- /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 000000000..ac64cbdb8 --- /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 fc9514bb2..fc45b694e 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 0ad0ede60..582d07e2c 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 000000000..296703bb2 --- /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 b702bffc3..31f6f82ee 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 47b479e2a..173714156 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 7ab059a76..e0d0134f2 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'),