diff --git a/conference.js b/conference.js index c812eb415..cc81e91b7 100644 --- a/conference.js +++ b/conference.js @@ -273,9 +273,11 @@ function muteLocalVideo(muted) { function maybeRedirectToWelcomePage(options) { // if close page is enabled redirect to it, without further action if (config.enableClosePage) { + const { isGuest } = APP.store.getState()['features/jwt']; + // save whether current user is guest or not, before navigating // to close page - window.sessionStorage.setItem('guest', APP.tokenData.isGuest); + window.sessionStorage.setItem('guest', isGuest); assignWindowLocationPathname('static/' + (options.feedbackSubmitted ? "close.html" : "close2.html")); return; diff --git a/connection.js b/connection.js index a827f1728..f98d49a88 100644 --- a/connection.js +++ b/connection.js @@ -1,5 +1,4 @@ /* global APP, JitsiMeetJS, config */ -const logger = require("jitsi-meet-logger").getLogger(__filename); import AuthHandler from './modules/UI/authentication/AuthHandler'; import jitsiLocalStorage from './modules/util/JitsiLocalStorage'; @@ -14,6 +13,7 @@ import { const ConnectionEvents = JitsiMeetJS.events.connection; const ConnectionErrors = JitsiMeetJS.errors.connection; +const logger = require("jitsi-meet-logger").getLogger(__filename); /** * Checks if we have data to use attach instead of connect. If we have the data @@ -61,22 +61,27 @@ function checkForAttachParametersAndConnect(id, password, connection) { * everything is ok, else error. */ function connect(id, password, roomName) { - - let connectionConfig = Object.assign({}, config); + const connectionConfig = Object.assign({}, config); + const { issuer, jwt } = APP.store.getState()['features/jwt']; connectionConfig.bosh += '?room=' + roomName; + let connection - = new JitsiMeetJS.JitsiConnection(null, config.token, connectionConfig); + = new JitsiMeetJS.JitsiConnection( + null, + jwt && issuer && issuer !== 'anonymous' ? jwt : undefined, + connectionConfig); return new Promise(function (resolve, reject) { connection.addEventListener( - ConnectionEvents.CONNECTION_ESTABLISHED, handleConnectionEstablished - ); + ConnectionEvents.CONNECTION_ESTABLISHED, + handleConnectionEstablished); connection.addEventListener( - ConnectionEvents.CONNECTION_FAILED, handleConnectionFailed - ); + ConnectionEvents.CONNECTION_FAILED, + handleConnectionFailed); connection.addEventListener( - ConnectionEvents.CONNECTION_FAILED, connectionFailedHandler); + ConnectionEvents.CONNECTION_FAILED, + connectionFailedHandler); function connectionFailedHandler(error, errMsg) { APP.store.dispatch(connectionFailed(connection, error, errMsg)); @@ -91,12 +96,10 @@ function connect(id, password, roomName) { function unsubscribe() { connection.removeEventListener( ConnectionEvents.CONNECTION_ESTABLISHED, - handleConnectionEstablished - ); + handleConnectionEstablished); connection.removeEventListener( ConnectionEvents.CONNECTION_FAILED, - handleConnectionFailed - ); + handleConnectionFailed); } function handleConnectionEstablished() { @@ -129,7 +132,6 @@ function connect(id, password, roomName) { * @returns {Promise} */ export function openConnection({id, password, retry, roomName}) { - let usernameOverride = jitsiLocalStorage.getItem("xmpp_username_override"); let passwordOverride @@ -138,25 +140,20 @@ export function openConnection({id, password, retry, roomName}) { if (usernameOverride && usernameOverride.length > 0) { id = usernameOverride; } - if (passwordOverride && passwordOverride.length > 0) { password = passwordOverride; } - return connect(id, password, roomName).catch(function (err) { - if (!retry) { - throw err; - } + return connect(id, password, roomName).catch(err => { + if (retry) { + const { issuer, jwt } = APP.store.getState()['features/jwt']; - if (err === ConnectionErrors.PASSWORD_REQUIRED) { - // do not retry if token is not valid - if (config.token) { - throw err; - } else { + if (err === ConnectionErrors.PASSWORD_REQUIRED + && (!jwt || issuer === 'anonymous')) { return AuthHandler.requestAuth(roomName, connect); } - } else { - throw err; } + + throw err; }); } diff --git a/connection_optimization/do_external_connect.js b/connection_optimization/do_external_connect.js index 52af8ecb5..c1d32c988 100644 --- a/connection_optimization/do_external_connect.js +++ b/connection_optimization/do_external_connect.js @@ -7,35 +7,29 @@ import { /** * Implements external connect using createConnectionExternally function defined - * in external_connect.js for Jitsi Meet. Parses the room name and token from - * the URL and executes createConnectionExternally. + * in external_connect.js for Jitsi Meet. Parses the room name and JSON Web + * Token (JWT) from the URL and executes createConnectionExternally. * - * NOTE: If you are using lib-jitsi-meet without Jitsi Meet you should use this + * NOTE: If you are using lib-jitsi-meet without Jitsi Meet, you should use this * file as reference only because the implementation is Jitsi Meet-specific. * * NOTE: For optimal results this file should be included right after * external_connect.js. */ -const hashParams = parseURLParams(window.location, true); +if (typeof createConnectionExternally === 'function') { + // URL params have higher proirity than config params. + let url + = parseURLParams(window.location, true, 'hash')[ + 'config.externalConnectUrl'] + || config.externalConnectUrl; + let roomName; -// URL params have higher proirity than config params. -let url = hashParams['config.externalConnectUrl'] || config.externalConnectUrl; - -if (url && window.createConnectionExternally) { - const roomName = getRoomName(); - - if (roomName) { + if (url && (roomName = getRoomName())) { url += `?room=${roomName}`; - let token = hashParams['config.token'] || config.token; + const token = parseURLParams(window.location, true, 'search').jwt; - if (!token) { - const searchParams - = parseURLParams(window.location, true, 'search'); - - token = searchParams.jwt; - } if (token) { url += `&token=${token}`; } diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 76ec2d15f..b61ed45ea 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -362,9 +362,9 @@ UI.start = function () { } - if(APP.tokenData.callee) { - UI.showRingOverlay(); - } + const { callee } = APP.store.getState()['features/jwt']; + + callee && UI.showRingOverlay(); }; /** @@ -1332,7 +1332,10 @@ UI.setMicrophoneButtonEnabled = enabled => APP.store.dispatch(setAudioIconEnabled(enabled)); UI.showRingOverlay = function () { - RingOverlay.show(APP.tokenData.callee, interfaceConfig.DISABLE_RINGING); + const { callee } = APP.store.getState()['features/jwt']; + + callee && RingOverlay.show(callee, interfaceConfig.DISABLE_RINGING); + Filmstrip.toggleFilmstrip(false, false); }; @@ -1397,7 +1400,13 @@ const UIListeners = new Map([ UI.toggleContactList ], [ UIEvents.TOGGLE_PROFILE, - () => APP.tokenData.isGuest && UI.toggleSidePanel("profile_container") + () => { + const { + isGuest + } = APP.store.getState()['features/jwt']; + + isGuest && UI.toggleSidePanel('profile_container'); + } ], [ UIEvents.TOGGLE_FILMSTRIP, UI.handleToggleFilmstrip diff --git a/modules/UI/authentication/AuthHandler.js b/modules/UI/authentication/AuthHandler.js index 6d4e0a121..74981f717 100644 --- a/modules/UI/authentication/AuthHandler.js +++ b/modules/UI/authentication/AuthHandler.js @@ -1,11 +1,13 @@ /* global APP, config, JitsiMeetJS, Promise */ -const logger = require("jitsi-meet-logger").getLogger(__filename); + +import { openConnection } from '../../../connection'; +import { setJWT } from '../../../react/features/jwt'; +import UIUtil from '../util/UIUtil'; import LoginDialog from './LoginDialog'; -import UIUtil from '../util/UIUtil'; -import {openConnection} from '../../../connection'; const ConnectionErrors = JitsiMeetJS.errors.connection; +const logger = require("jitsi-meet-logger").getLogger(__filename); let externalAuthWindow; let authRequiredDialog; @@ -73,15 +75,20 @@ function redirectToTokenAuthService(roomName) { * @param room the name fo the conference room. */ function initJWTTokenListener(room) { - var listener = function (event) { - if (externalAuthWindow !== event.source) { + var listener = function ({ data, source }) { + if (externalAuthWindow !== source) { logger.warn("Ignored message not coming " + "from external authnetication window"); return; } - if (event.data && event.data.jwtToken) { - config.token = event.data.jwtToken; - logger.info("Received JWT token:", config.token); + + let jwt; + + if (data && (jwt = data.jwtToken)) { + logger.info("Received JSON Web Token (JWT):", jwt); + + APP.store.dispatch(setJWT(jwt)); + var roomName = room.getName(); openConnection({retry: false, roomName: roomName }) .then(function (connection) { diff --git a/modules/UI/ring_overlay/RingOverlay.js b/modules/UI/ring_overlay/RingOverlay.js index a59b83871..76ec0fe26 100644 --- a/modules/UI/ring_overlay/RingOverlay.js +++ b/modules/UI/ring_overlay/RingOverlay.js @@ -22,7 +22,8 @@ function onAvatarVisible(shown) { */ class RingOverlay { /** - * @param callee instance of User class from TokenData.js + * + * @param callee The callee (Object) as defined by the JWT support. * @param {boolean} disableRingingSound if true the ringing sound wont be played. */ constructor(callee, disableRingingSound) { @@ -77,9 +78,9 @@ class RingOverlay {
${callingLabel} - +
-

${callee.getName()}${callerStateLabel}

+

${callee.name}${callerStateLabel}

${audioHTML} @@ -137,9 +138,12 @@ class RingOverlay { export default { /** * Shows the ring overlay for the passed callee. - * @param callee {class User} the callee. Instance of User class from - * TokenData.js - * @param {boolean} disableRingingSound if true the ringing sound wont be played. + * + * @param {Object} callee - The callee. Object containing data about + * callee. + * @param {boolean} disableRingingSound - If true the ringing sound won't be + * played. + * @returns {void} */ show(callee, disableRingingSound = false) { if(overlay) { diff --git a/modules/analytics/analytics.js b/modules/analytics/analytics.js index 219fbae57..e83681163 100644 --- a/modules/analytics/analytics.js +++ b/modules/analytics/analytics.js @@ -1,4 +1,5 @@ /* global JitsiMeetJS, config, APP */ + /** * Load the integration of a third-party analytics API such as Google * Analytics. Since we cannot guarantee the quality of the third-party service @@ -101,26 +102,31 @@ class Analytics { * null. */ init() { - let analytics = JitsiMeetJS.analytics; - if(!this.isEnabled() || !analytics) + const { analytics } = JitsiMeetJS; + + if (!this.isEnabled() || !analytics) return; - this._loadHandlers() - .then(handlers => { - let permanentProperties = { - userAgent: navigator.userAgent, - roomName: APP.conference.roomName + this._loadHandlers().then( + handlers => { + const permanentProperties = { + roomName: APP.conference.roomName, + userAgent: navigator.userAgent }; - let {server, group} = APP.tokenData; - if(server) { + + const { group, server } = APP.store.getState()['features/jwt']; + + if (server) { permanentProperties.server = server; } - if(group) { + if (group) { permanentProperties.group = group; } + analytics.addPermanentProperties(permanentProperties); analytics.setAnalyticsHandlers(handlers); - }, error => analytics.dispose() && console.error(error)); + }, + error => analytics.dispose() && console.error(error)); } } diff --git a/modules/tokendata/TokenData.js b/modules/tokendata/TokenData.js deleted file mode 100644 index 7b879333d..000000000 --- a/modules/tokendata/TokenData.js +++ /dev/null @@ -1,123 +0,0 @@ -/* global config */ - -/** - * Parses and handles JWT tokens. Sets config.token. - */ - -import * as jws from "jws"; - -import { getConfigParamsFromUrl } from '../../react/features/base/config'; - -/** - * Get the JWT token from the URL. - */ -let params = getConfigParamsFromUrl(window.location, true, 'search'); -let jwt = params.jwt; - -/** - * Implements a user of conference. - */ -class User { - /** - * @param name {string} the name of the user. - * @param email {string} the email of the user. - * @param avatarUrl {string} the URL for the avatar of the user. - */ - constructor(name, email, avatarUrl) { - this._name = name; - this._email = email; - this._avatarUrl = avatarUrl; - } - - /** - * GETERS START. - */ - - /** - * Returns the name property - */ - getName() { - return this._name; - } - - /** - * Returns the email property - */ - getEmail() { - return this._email; - } - - /** - * Returns the URL of the avatar - */ - getAvatarUrl() { - return this._avatarUrl; - } - - /** - * GETERS END. - */ -} - -/** - * Represent the data parsed from the JWT token - */ -class TokenData{ - /** - * @param {string} the JWT token - */ - constructor(jwt) { - this.isGuest = true; - if(!jwt) - return; - - this.isGuest = config.enableUserRolesBasedOnToken !== true; - - this.jwt = jwt; - - this._decode(); - // Use JWT param as token if there is not other token set and if the - // iss field is not anonymous. If you want to pass data with JWT token - // but you don't want to pass the JWT token for verification the iss - // field should be set to "anonymous" - if(!config.token && this.payload && this.payload.iss !== "anonymous") - config.token = jwt; - } - - /** - * Decodes the JWT token and sets the decoded data to properties. - */ - _decode() { - this.decodedJWT = jws.decode(jwt); - if(!this.decodedJWT || !this.decodedJWT.payload) - return; - this.payload = this.decodedJWT.payload; - if(!this.payload.context) - return; - this.server = this.payload.context.server; - this.group = this.payload.context.group; - let callerData = this.payload.context.user; - let calleeData = this.payload.context.callee; - if(callerData) - this.caller = new User(callerData.name, callerData.email, - callerData.avatarUrl); - if(calleeData) - this.callee = new User(calleeData.name, calleeData.email, - calleeData.avatarUrl); - } -} - -/** - * Stores the TokenData instance. - */ -let data = null; - -/** - * Returns the data variable. Creates new TokenData instance if data - * variable is null. - */ -export default function getTokenData() { - if(!data) - data = new TokenData(jwt); - return data; -} diff --git a/package.json b/package.json index a0f53e862..585f54708 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "jQuery-Impromptu": "trentrichardson/jQuery-Impromptu#v6.0.0", "jquery-ui": "1.10.5", "jssha": "1.5.0", - "jws": "3.1.4", + "jwt-decode": "2.2.0", "lib-jitsi-meet": "jitsi/lib-jitsi-meet", "lodash": "4.17.4", "postis": "2.2.0", diff --git a/react/features/app/actions.js b/react/features/app/actions.js index 3fef524e1..fac887a23 100644 --- a/react/features/app/actions.js +++ b/react/features/app/actions.js @@ -1,4 +1,4 @@ -import { setRoom, setRoomUrl } from '../base/conference'; +import { setRoom, setRoomURL } from '../base/conference'; import { setConfig } from '../base/config'; import { getDomain, setDomain } from '../base/connection'; import { loadConfig } from '../base/lib-jitsi-meet'; @@ -18,7 +18,7 @@ import { * @returns {Function} */ export function appInit() { - return () => init(); + return (dispatch, getState) => init(getState()); } /** @@ -55,7 +55,7 @@ export function appNavigate(uri) { urlObject = new URL(urlWithoutDomain, `https://${domain}`); } - dispatch(setRoomUrl(urlObject)); + dispatch(setRoomURL(urlObject)); // TODO Kostiantyn Tsaregradskyi: We should probably detect if user is // currently in a conference and ask her if she wants to close the diff --git a/react/features/app/functions.web.js b/react/features/app/functions.web.js index eec0ecb4c..cd6b9cd91 100644 --- a/react/features/app/functions.web.js +++ b/react/features/app/functions.web.js @@ -16,7 +16,6 @@ import { WelcomePage } from '../welcome'; import KeyboardShortcut from '../../../modules/keyboardshortcut/keyboardshortcut'; -import getTokenData from '../../../modules/tokendata/TokenData'; import JitsiMeetLogStorage from '../../../modules/util/JitsiMeetLogStorage'; declare var APP: Object; @@ -111,18 +110,20 @@ export function _getRouteToRender(stateOrGetState: Object | Function) { * Temporary solution. Later we'll get rid of global APP and set its properties * in redux store. * + * @param {Object} state - Snapshot of current state of redux store. * @returns {void} */ -export function init() { +export function init(state: Object) { _initLogging(); APP.keyboardshortcut = KeyboardShortcut; - APP.tokenData = getTokenData(); + + const { jwt } = state['features/jwt']; // Force enable the API if jwt token is passed because most probably // jitsi meet is displayed inside of wrapper that will need to communicate // with jitsi meet. - APP.API.init(APP.tokenData.jwt ? { forceEnable: true } : undefined); + APP.API.init(jwt ? { forceEnable: true } : undefined); APP.translation.init(); } diff --git a/react/features/app/index.js b/react/features/app/index.js index 5d564613e..cb41009ea 100644 --- a/react/features/app/index.js +++ b/react/features/app/index.js @@ -3,4 +3,9 @@ export * from './actionTypes'; export * from './components'; export * from './functions'; +// We need to import the jwt module in order to register the reducer and +// middleware, because the module is not used outside of this feature. +import '../jwt'; + import './reducer'; + diff --git a/react/features/base/react/components/Watermarks.web.js b/react/features/base/react/components/Watermarks.web.js index ed1b49bde..9da8c27f0 100644 --- a/react/features/base/react/components/Watermarks.web.js +++ b/react/features/base/react/components/Watermarks.web.js @@ -1,10 +1,10 @@ /* @flow */ import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { translate } from '../../i18n'; -declare var APP: Object; declare var interfaceConfig: Object; /** @@ -131,7 +131,7 @@ class Watermarks extends Component { let reactElement = null; if (this.state.showJitsiWatermark - || (APP.tokenData.isGuest + || (this.props._isGuest && this.state.showJitsiWatermarkForGuests)) { reactElement =
; @@ -175,4 +175,27 @@ class Watermarks extends Component { } } -export default translate(Watermarks); +/** + * Maps parts of Redux store to component prop types. + * + * @param {Object} state - Snapshot of Redux store. + * @returns {{ + * _isGuest: boolean + * }} + */ +function _mapStateToProps(state) { + const { isGuest } = state['features/jwt']; + + return { + /** + * The indicator which determines whether the local participant is a + * guest in the conference. + * + * @private + * @type {boolean} + */ + _isGuest: isGuest + }; +} + +export default connect(_mapStateToProps)(translate(Watermarks)); diff --git a/react/features/conference/route.js b/react/features/conference/route.js index 7515f6b0b..86036a3f5 100644 --- a/react/features/conference/route.js +++ b/react/features/conference/route.js @@ -113,12 +113,13 @@ function _obtainConfigHandler() { * @returns {void} */ function _setTokenData() { - const localUser = APP.tokenData.caller; + const state = APP.store.getState(); + const { caller } = state['features/jwt']; - if (localUser) { - const email = localUser.getEmail(); - const avatarUrl = localUser.getAvatarUrl(); - const name = localUser.getName(); + if (caller) { + const email = caller.email; + const avatarUrl = caller.avatarUrl; + const name = caller.name; APP.settings.setEmail((email || '').trim(), true); APP.settings.setAvatarUrl((avatarUrl || '').trim()); diff --git a/react/features/jwt/actionTypes.js b/react/features/jwt/actionTypes.js new file mode 100644 index 000000000..8b21d65b0 --- /dev/null +++ b/react/features/jwt/actionTypes.js @@ -0,0 +1,12 @@ +import { Symbol } from '../base/react'; + +/** + * The type of redux action which stores a specific JSON Web Token (JWT) into + * the redux store. + * + * { + * type: SET_JWT, + * jwt: string + * } + */ +export const SET_JWT = Symbol('SET_JWT'); diff --git a/react/features/jwt/actions.js b/react/features/jwt/actions.js new file mode 100644 index 000000000..4710b75ed --- /dev/null +++ b/react/features/jwt/actions.js @@ -0,0 +1,19 @@ +/* @flow */ + +import { SET_JWT } from './actionTypes'; + +/** + * Stores a specific JSON Web Token (JWT) into the redux store. + * + * @param {string} jwt - The JSON Web Token (JWT) to store. + * @returns {{ + * type: SET_TOKEN_DATA, + * jwt: string + * }} + */ +export function setJWT(jwt: string) { + return { + type: SET_JWT, + jwt + }; +} diff --git a/react/features/jwt/index.js b/react/features/jwt/index.js new file mode 100644 index 000000000..dbf9c8839 --- /dev/null +++ b/react/features/jwt/index.js @@ -0,0 +1,4 @@ +export * from './actions'; + +import './middleware'; +import './reducer'; diff --git a/react/features/jwt/middleware.js b/react/features/jwt/middleware.js new file mode 100644 index 000000000..2e1d767ee --- /dev/null +++ b/react/features/jwt/middleware.js @@ -0,0 +1,106 @@ +import jwtDecode from 'jwt-decode'; + +import { SET_ROOM_URL } from '../base/conference'; +import { parseURLParams, SET_CONFIG } from '../base/config'; +import { MiddlewareRegistry } from '../base/redux'; + +import { setJWT } from './actions'; +import { SET_JWT } from './actionTypes'; + +/** + * Middleware to parse token data upon setting a new room URL. + * + * @param {Store} store - The Redux store. + * @private + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case SET_CONFIG: + case SET_ROOM_URL: + // XXX The JSON Web Token (JWT) is not the only piece of state that we + // have decided to store in the feature jwt, there is isGuest as well + // which depends on the states of the features base/config and jwt. So + // the JSON Web Token comes from the room's URL and isGuest needs a + // recalculation upon SET_CONFIG as well. + return _setConfigOrRoomURL(store, next, action); + + case SET_JWT: + return _setJWT(store, next, action); + } + + return next(action); +}); + +/** + * Notifies the feature jwt that the action {@link SET_CONFIG} or + * {@link SET_ROOM_URL} is being dispatched within a specific Redux + * {@code store}. + * + * @param {Store} store - The Redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The Redux dispatch function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The Redux action {@code SET_CONFIG} or + * {@code SET_ROOM_NAME} which is being dispatched in the specified + * {@code store}. + * @private + * @returns {Object} The new state that is the result of the reduction of the + * specified {@code action}. + */ +function _setConfigOrRoomURL({ dispatch, getState }, next, action) { + const result = next(action); + + const { roomURL } = getState()['features/base/conference']; + let jwt; + + if (roomURL) { + jwt = parseURLParams(roomURL, true, 'search').jwt; + } + dispatch(setJWT(jwt)); + + return result; +} + +/** + * Notifies the feature jwt that the action {@link SET_JWT} is being dispatched + * within a specific Redux {@code store}. + * + * @param {Store} store - The Redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The Redux dispatch function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The Redux action {@code SET_JWT} which is being + * dispatched in the specified {@code store}. + * @private + * @returns {Object} The new state that is the result of the reduction of the + * specified {@code action}. + */ +function _setJWT({ getState }, next, action) { + // eslint-disable-next-line no-unused-vars + const { jwt, type, ...actionPayload } = action; + + if (jwt && !Object.keys(actionPayload).length) { + const { + enableUserRolesBasedOnToken + } = getState()['features/base/config']; + + action.isGuest = !enableUserRolesBasedOnToken; + + const jwtPayload = jwtDecode(jwt); + + if (jwtPayload) { + const { context, iss } = jwtPayload; + + action.issuer = iss; + if (context) { + action.callee = context.callee; + action.caller = context.user; + action.group = context.group; + action.server = context.server; + } + } + } + + return next(action); +} diff --git a/react/features/jwt/reducer.js b/react/features/jwt/reducer.js new file mode 100644 index 000000000..92fa32fbf --- /dev/null +++ b/react/features/jwt/reducer.js @@ -0,0 +1,47 @@ +import { equals, ReducerRegistry } from '../base/redux'; + +import { SET_JWT } from './actionTypes'; + +/** + * The initial redux state of the feature jwt. + * + * @private + * @type {{ + * isGuest: boolean + * }} + */ +const _INITIAL_STATE = { + /** + * The indicator which determines whether the local participant is a guest + * in the conference. + * + * @type {boolean} + */ + isGuest: true +}; + +/** + * Reduces redux actions which affect the JSON Web Token (JWT) stored in the + * redux store. + * + * @param {Object} state - The current redux state. + * @param {Object} action - The redux action to reduce. + * @returns {Object} The next redux state which is the result of reducing the + * specified {@code action}. + */ +ReducerRegistry.register('features/jwt', (state = _INITIAL_STATE, action) => { + switch (action.type) { + case SET_JWT: { + // eslint-disable-next-line no-unused-vars + const { type, ...payload } = action; + const nextState = { + ..._INITIAL_STATE, + ...payload + }; + + return equals(state, nextState) ? state : nextState; + } + } + + return state; +}); diff --git a/react/features/toolbox/components/SecondaryToolbar.web.js b/react/features/toolbox/components/SecondaryToolbar.web.js index 2bef38ebf..59ce8dbb1 100644 --- a/react/features/toolbox/components/SecondaryToolbar.web.js +++ b/react/features/toolbox/components/SecondaryToolbar.web.js @@ -33,6 +33,12 @@ class SecondaryToolbar extends Component { * @static */ static propTypes = { + /** + * The indicator which determines whether the local participant is a + * guest in the conference. + */ + _isGuest: React.PropTypes.bool, + /** * Handler dispatching local "Raise hand". */ @@ -79,9 +85,14 @@ class SecondaryToolbar extends Component { * @type {Object} */ profile: { - onMount: () => - APP.tokenData.isGuest - || this.props._onSetProfileButtonUnclickable(true) + onMount: () => { + const { + _isGuest, + _onSetProfileButtonUnclickable + } = this.props; + + _isGuest || _onSetProfileButtonUnclickable(true); + } }, /** @@ -237,18 +248,26 @@ function _mapDispatchToProps(dispatch: Function): Object { * * @param {Object} state - Snapshot of Redux store. * @returns {{ + * _isGuest: boolean, * _secondaryToolbarButtons: Map, * _visible: boolean * }} * @private */ function _mapStateToProps(state: Object): Object { - const { - secondaryToolbarButtons, - visible - } = state['features/toolbox']; + const { isGuest } = state['features/jwt']; + const { secondaryToolbarButtons, visible } = state['features/toolbox']; return { + /** + * The indicator which determines whether the local participant is a + * guest in the conference. + * + * @private + * @type {boolean} + */ + _isGuest: isGuest, + /** * Default toolbar buttons for secondary toolbar. * @@ -258,7 +277,8 @@ function _mapStateToProps(state: Object): Object { _secondaryToolbarButtons: secondaryToolbarButtons, /** - * Shows whether toolbar is visible. + * The indicator which determines whether the {@code SecondaryToolbar} + * is visible. * * @private * @type {boolean}