Token based features (#3075)

* Adds an option to disable features based on token data.

Reverts changes from b84e910086, removes disableDesktopSharing option and an interface_config option.

* Disable recording button based on token features data.

Hide recording if local participant isGuest and roles based on token.
When enableUserRolesBasedOnToken is enabled we were not hiding the record button for guests.

* Adds filtering of jibri iqs and rayo based on features.

Moves feature checking in separate utility function.
Renames utility method.

* Adds a footer text when outbound-call is not feature enabled.

* Fixes comments.
This commit is contained in:
Дамян Минков
2018-06-15 13:10:22 -05:00
committed by GitHub
parent 0cf585860b
commit ac834326e7
18 changed files with 402 additions and 108 deletions
+16 -2
View File
@@ -188,8 +188,17 @@ function _reportError(msg, err) {
*/
export function sendLocalParticipant(
stateful: Function | Object,
conference: { sendCommand: Function, setDisplayName: Function }) {
const { avatarID, avatarURL, email, name } = getLocalParticipant(stateful);
conference: {
sendCommand: Function,
setDisplayName: Function,
setLocalParticipantProperty: Function }) {
const {
avatarID,
avatarURL,
email,
features,
name
} = getLocalParticipant(stateful);
avatarID && conference.sendCommand(AVATAR_ID_COMMAND, {
value: avatarID
@@ -200,5 +209,10 @@ export function sendLocalParticipant(
email && conference.sendCommand(EMAIL_COMMAND, {
value: email
});
if (features && features['screen-sharing'] === 'true') {
conference.setLocalParticipantProperty('features_screen-sharing', true);
}
conference.setDisplayName(name);
}
-3
View File
@@ -89,8 +89,6 @@ const WHITELISTED_KEYS = [
'disableAGC',
'disableAP',
'disableAudioLevels',
'disableDesktopSharing',
'disableDesktopSharing',
'disableH264',
'disableHPF',
'disableNS',
@@ -106,7 +104,6 @@ const WHITELISTED_KEYS = [
'enableStatsID',
'enableTalkWhileMuted',
'enableTcc',
'enableUserRolesBasedOnToken',
'etherpad_base',
'failICE',
'fileRecordingsEnabled',
+9 -2
View File
@@ -138,7 +138,7 @@ function _maybeSetCalleeInfoVisible({ dispatch, getState }, next, action) {
*/
function _overwriteLocalParticipant(
{ dispatch, getState },
{ avatarURL, email, name }) {
{ avatarURL, email, name, features }) {
let localParticipant;
if ((avatarURL || email || name)
@@ -157,6 +157,9 @@ function _overwriteLocalParticipant(
if (name) {
newProperties.name = name;
}
if (features) {
newProperties.features = features;
}
dispatch(participantUpdated(newProperties));
}
}
@@ -229,7 +232,9 @@ function _setJWT(store, next, action) {
action.server = context.server;
action.user = user;
user && _overwriteLocalParticipant(store, user);
user && _overwriteLocalParticipant(
store, { ...user,
features: context.features });
}
}
} else if (typeof APP === 'undefined') {
@@ -281,6 +286,8 @@ function _undoOverwriteLocalParticipant(
if (name === localParticipant.name) {
newProperties.name = undefined;
}
newProperties.features = undefined;
dispatch(participantUpdated(newProperties));
}
}
@@ -24,6 +24,12 @@ class MultiSelectAutocomplete extends Component {
*/
defaultValue: PropTypes.array,
/**
* Optional footer to show as a last element in the results.
* Should be of type {content: <some content>}
*/
footer: PropTypes.object,
/**
* Indicates if the component is disabled.
*/
@@ -151,6 +157,7 @@ class MultiSelectAutocomplete extends Component {
<div>
<MultiSelectStateless
filterValue = { this.state.filterValue }
footer = { this.props.footer }
icon = { null }
isDisabled = { isDisabled }
isLoading = { this.state.loading }
@@ -8,7 +8,8 @@ import { connect } from 'react-redux';
import { createInviteDialogEvent, sendAnalytics } from '../../analytics';
import { Dialog, hideDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { translate, translateToHTML } from '../../base/i18n';
import { getLocalParticipant } from '../../base/participants';
import { MultiSelectAutocomplete } from '../../base/react';
import { invite } from '../actions';
@@ -39,6 +40,12 @@ class AddPeopleDialog extends Component<*, *> {
*/
_dialOutAuthUrl: PropTypes.string,
/**
* Whether to show a footer text after the search results
* as a last element.
*/
_footerTextEnabled: PropTypes.bool,
/**
* The JWT token.
*/
@@ -168,11 +175,15 @@ class AddPeopleDialog extends Component<*, *> {
* @returns {ReactElement}
*/
render() {
const { addPeopleEnabled, dialOutEnabled, t } = this.props;
const { _footerTextEnabled,
addPeopleEnabled,
dialOutEnabled,
t } = this.props;
let isMultiSelectDisabled = this.state.addToCallInProgress || false;
let placeholder;
let loadingMessage;
let noMatches;
let footerText;
if (addPeopleEnabled && dialOutEnabled) {
loadingMessage = 'addPeople.loading';
@@ -192,6 +203,19 @@ class AddPeopleDialog extends Component<*, *> {
placeholder = 'addPeople.disabled';
}
if (_footerTextEnabled) {
footerText = {
content: <div className = 'footer-text-wrap'>
<div>
<span className = 'footer-telephone-icon'>
<i className = 'icon-telephone' />
</span>
</div>
{ translateToHTML(t, 'addPeople.footerText') }
</div>
};
}
return (
<Dialog
okDisabled = { this._isAddDisabled() }
@@ -202,6 +226,7 @@ class AddPeopleDialog extends Component<*, *> {
<div className = 'add-people-form-wrap'>
{ this._renderErrorMessage() }
<MultiSelectAutocomplete
footer = { footerText }
isDisabled = { isMultiSelectDisabled }
loadingMessage = { t(loadingMessage) }
noMatchesFound = { t(noMatches) }
@@ -525,12 +550,23 @@ class AddPeopleDialog extends Component<*, *> {
function _mapStateToProps(state) {
const {
dialOutAuthUrl,
enableFeaturesBasedOnToken,
peopleSearchQueryTypes,
peopleSearchUrl
} = state['features/base/config'];
let footerTextEnabled = false;
if (enableFeaturesBasedOnToken) {
const { features = {} } = getLocalParticipant(state);
if (String(features['outbound-call']) !== 'true') {
footerTextEnabled = true;
}
}
return {
_dialOutAuthUrl: dialOutAuthUrl,
_footerTextEnabled: footerTextEnabled,
_jwt: state['features/base/jwt'].jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl
@@ -1,3 +1,4 @@
import Tooltip from '@atlaskit/tooltip';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
@@ -8,6 +9,16 @@ import React, { Component } from 'react';
* @extends Component
*/
class OverflowMenuItem extends Component {
/**
* Default values for {@code OverflowMenuItem} component's properties.
*
* @static
*/
static defaultProps = {
tooltipPosition: 'left',
disabled: false
};
/**
* {@code OverflowMenuItem} component's property types.
*
@@ -20,6 +31,11 @@ class OverflowMenuItem extends Component {
*/
accessibilityLabel: PropTypes.string,
/**
* Whether menu item is disabled or not.
*/
disabled: PropTypes.bool,
/**
* The icon class to use for displaying an icon before the link text.
*/
@@ -33,7 +49,18 @@ class OverflowMenuItem extends Component {
/**
* The text to display in the {@code OverflowMenuItem}.
*/
text: PropTypes.string
text: PropTypes.string,
/**
* The text to display in the tooltip.
*/
tooltip: PropTypes.string,
/**
* From which direction the tooltip should appear, relative to the
* button.
*/
tooltipPosition: PropTypes.string
};
/**
@@ -43,15 +70,25 @@ class OverflowMenuItem extends Component {
* @returns {ReactElement}
*/
render() {
let className = 'overflow-menu-item';
className += this.props.disabled ? ' disabled' : '';
return (
<li
aria-label = { this.props.accessibilityLabel }
className = 'overflow-menu-item'
onClick = { this.props.onClick }>
className = { className }
onClick = { this.props.disabled ? null : this.props.onClick }>
<span className = 'overflow-menu-item-icon'>
<i className = { this.props.icon } />
</span>
{ this.props.text }
{ this.props.tooltip
? <Tooltip
content = { this.props.tooltip }
position = { this.props.tooltipPosition }>
<span>{ this.props.text }</span>
</Tooltip>
: this.props.text }
</li>
);
}
@@ -1,39 +1,66 @@
// @flow
import Tooltip from '@atlaskit/tooltip';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
/**
* The type of the React {@code Component} props of
* {@link OverflowMenuLiveStreamingItem}.
*/
type Props = {
/**
* The callback to invoke when {@code OverflowMenuLiveStreamingItem} is
* clicked.
*/
onClick: Function,
/**
* The current live streaming session, if any.
*/
session: ?Object,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* React {@code Component} for starting or stopping a live streaming of the
* current conference and displaying the current state of live streaming.
*
* @extends Component
*/
class OverflowMenuLiveStreamingItem extends Component<Props> {
class OverflowMenuLiveStreamingItem extends Component {
/**
* Default values for {@code OverflowMenuLiveStreamingItem}
* component's properties.
*
* @static
*/
static defaultProps = {
tooltipPosition: 'left',
disabled: false
};
/**
* The type of the React {@code Component} props of
* {@link OverflowMenuLiveStreamingItem}.
*/
static propTypes = {
/**
* Whether menu item is disabled or not.
*/
disabled: PropTypes.bool,
/**
* The callback to invoke when {@code OverflowMenuLiveStreamingItem} is
* clicked.
*/
onClick: Function,
/**
* The current live streaming session, if any.
*/
session: PropTypes.object,
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* The text to display in the tooltip.
*/
tooltip: PropTypes.string,
/**
* From which direction the tooltip should appear, relative to the
* button.
*/
tooltipPosition: PropTypes.string
};
/**
* Implements React's {@link Component#render()}.
*
@@ -41,26 +68,43 @@ class OverflowMenuLiveStreamingItem extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { onClick, session, t } = this.props;
const { disabled, onClick, session, t, tooltip, tooltipPosition }
= this.props;
const translationKey = session
? 'dialog.stopLiveStreaming'
: 'dialog.startLiveStreaming';
return (
<li
aria-label = { t('dialog.accessibilityLabel.liveStreaming') }
className = 'overflow-menu-item'
onClick = { onClick }>
<span className = 'overflow-menu-item-icon'>
<i className = 'icon-public' />
</span>
let className = 'overflow-menu-item';
className += disabled ? ' disabled' : '';
const button = (// eslint-disable-line no-extra-parens
<span>
<span className = 'profile-text'>
{ t(translationKey) }
</span>
<span className = 'beta-tag'>
{ t('recording.beta') }
</span>
</span>
);
return (
<li
aria-label = { t('dialog.accessibilityLabel.liveStreaming') }
className = { className }
onClick = { disabled ? null : onClick }>
<span className = 'overflow-menu-item-icon'>
<i className = 'icon-public' />
</span>
{ tooltip
? <Tooltip
content = { tooltip }
position = { tooltipPosition }>
{ button }
</Tooltip>
: button }
</li>
);
}
+111 -27
View File
@@ -13,8 +13,9 @@ import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import {
PARTICIPANT_ROLE,
getLocalParticipant,
getParticipants,
isLocalParticipantModerator,
participantUpdated
} from '../../../base/participants';
import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
@@ -74,9 +75,10 @@ type Props = {
_conference: Object,
/**
* Whether or not desktopsharing was explicitly configured to be disabled.
* The tooltip key to use when screensharing is disabled. Or undefined
* if non to be shown and the button to be hidden.
*/
_desktopSharingDisabledByConfig: boolean,
_desktopSharingDisabledTooltipKey: boolean,
/**
* Whether or not screensharing is initialized.
@@ -103,6 +105,12 @@ type Props = {
*/
_feedbackConfigured: boolean,
/**
* The tooltip key to use when file recording is disabled. Or undefined
* if non to be shown and the button to be hidden.
*/
_fileRecordingsDisabledTooltipKey: boolean,
/**
* Whether or not the file recording feature is enabled for use.
*/
@@ -129,6 +137,12 @@ type Props = {
*/
_isGuest: boolean,
/**
* The tooltip key to use when live streaming is disabled. Or undefined
* if non to be shown and the button to be hidden.
*/
_liveStreamingDisabledTooltipKey: boolean,
/**
* Whether or not the live streaming feature is enabled for use.
*/
@@ -925,17 +939,14 @@ class Toolbox extends Component<Props> {
*/
_renderDesktopSharingButton() {
const {
_desktopSharingDisabledByConfig,
_desktopSharingEnabled,
_desktopSharingDisabledTooltipKey,
_screensharing,
t
} = this.props;
const disabledTooltipText
= interfaceConfig.DESKTOP_SHARING_BUTTON_DISABLED_TOOLTIP;
const showDisabledTooltip
= disabledTooltipText && _desktopSharingDisabledByConfig;
const visible = _desktopSharingEnabled || showDisabledTooltip;
const visible
= _desktopSharingEnabled || _desktopSharingDisabledTooltipKey;
if (!visible) {
return null;
@@ -944,9 +955,9 @@ class Toolbox extends Component<Props> {
const classNames = `icon-share-desktop ${
_screensharing ? 'toggled' : ''} ${
_desktopSharingEnabled ? '' : 'disabled'}`;
const tooltip = showDisabledTooltip
? disabledTooltipText
: t('dialog.shareYourScreen');
const tooltip = t(
_desktopSharingEnabled
? 'dialog.shareYourScreen' : _desktopSharingDisabledTooltipKey);
return (
<ToolbarButton
@@ -969,9 +980,11 @@ class Toolbox extends Component<Props> {
_editingDocument,
_etherpadInitialized,
_feedbackConfigured,
_fileRecordingsDisabledTooltipKey,
_fileRecordingsEnabled,
_fullScreen,
_isGuest,
_liveStreamingDisabledTooltipKey,
_liveStreamingEnabled,
_liveStreamingSession,
_sharingVideo,
@@ -1000,13 +1013,15 @@ class Toolbox extends Component<Props> {
text = { _fullScreen
? t('toolbar.exitFullScreen')
: t('toolbar.enterFullScreen') } />,
_liveStreamingEnabled
(_liveStreamingEnabled || _liveStreamingDisabledTooltipKey)
&& this._shouldShowButton('livestreaming')
&& <OverflowMenuLiveStreamingItem
disabled = { !_liveStreamingEnabled }
key = 'livestreaming'
onClick = { this._onToolbarToggleLiveStreaming }
session = { _liveStreamingSession } />,
_fileRecordingsEnabled
session = { _liveStreamingSession }
tooltip = { t(_liveStreamingDisabledTooltipKey) } />,
(_fileRecordingsEnabled || _fileRecordingsDisabledTooltipKey)
&& this._shouldShowButton('recording')
&& this._renderRecordingButton(),
this._shouldShowButton('sharedvideo')
@@ -1070,7 +1085,11 @@ class Toolbox extends Component<Props> {
* @returns {ReactElement|null}
*/
_renderRecordingButton() {
const { _fileRecordingSession, t } = this.props;
const {
_fileRecordingSession,
_fileRecordingsDisabledTooltipKey,
_fileRecordingsEnabled,
t } = this.props;
const translationKey = _fileRecordingSession
? 'dialog.stopRecording'
@@ -1080,10 +1099,12 @@ class Toolbox extends Component<Props> {
<OverflowMenuItem
accessibilityLabel =
{ t('toolbar.accessibilityLabel.recording') }
disabled = { !_fileRecordingsEnabled }
icon = 'icon-camera-take-picture'
key = 'recording'
onClick = { this._onToolbarToggleRecording }
text = { t(translationKey) } />
text = { t(translationKey) }
tooltip = { t(_fileRecordingsDisabledTooltipKey) } />
);
}
@@ -1111,15 +1132,14 @@ class Toolbox extends Component<Props> {
* @returns {{}}
*/
function _mapStateToProps(state) {
const {
conference,
desktopSharingEnabled
} = state['features/base/conference'];
const { conference } = state['features/base/conference'];
let { desktopSharingEnabled } = state['features/base/conference'];
const {
callStatsID,
disableDesktopSharing,
iAmRecorder
} = state['features/base/config'];
let {
fileRecordingsEnabled,
iAmRecorder,
liveStreamingEnabled
} = state['features/base/config'];
const sharedVideoStatus = state['features/shared-video'].status;
@@ -1133,15 +1153,77 @@ function _mapStateToProps(state) {
} = state['features/toolbox'];
const localParticipant = getLocalParticipant(state);
const localVideo = getLocalVideoTrack(state['features/base/tracks']);
const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
const addPeopleEnabled = isAddPeopleEnabled(state);
const dialOutEnabled = isDialOutEnabled(state);
let desktopSharingDisabledTooltipKey;
let fileRecordingsDisabledTooltipKey;
let liveStreamingDisabledTooltipKey;
fileRecordingsEnabled
= isLocalParticipantModerator(state) && fileRecordingsEnabled;
liveStreamingEnabled
= isLocalParticipantModerator(state) && liveStreamingEnabled;
if (state['features/base/config'].enableFeaturesBasedOnToken) {
// we enable desktop sharing if any participant already have this
// feature enabled
desktopSharingEnabled = getParticipants(state)
.find(({ features = {} }) =>
String(features['screen-sharing']) === 'true') !== undefined;
// we want to show button and tooltip
if (state['features/base/jwt'].isGuest) {
desktopSharingDisabledTooltipKey
= 'dialog.shareYourScreenDisabledForGuest';
} else {
desktopSharingDisabledTooltipKey
= 'dialog.shareYourScreenDisabled';
}
// we enable recording if the local participant have this
// feature enabled
const { features = {} } = localParticipant;
const { isGuest } = state['features/base/jwt'];
fileRecordingsEnabled
= fileRecordingsEnabled && String(features.recording) === 'true';
// if the feature is disabled on purpose, do no show it, no tooltip
if (!fileRecordingsEnabled
&& String(features.recording) !== 'disabled') {
// button and tooltip
if (isGuest) {
fileRecordingsDisabledTooltipKey
= 'dialog.recordingDisabledForGuestTooltip';
} else {
fileRecordingsDisabledTooltipKey
= 'dialog.recordingDisabledTooltip';
}
}
liveStreamingEnabled
= liveStreamingEnabled && String(features.livestreaming) === 'true';
// if the feature is disabled on purpose, do no show it, no tooltip
if (!liveStreamingEnabled
&& String(features.livestreaming) !== 'disabled') {
// button and tooltip
if (isGuest) {
liveStreamingDisabledTooltipKey
= 'dialog.liveStreamingDisabledForGuestTooltip';
} else {
liveStreamingDisabledTooltipKey
= 'dialog.liveStreamingDisabledTooltip';
}
}
}
return {
_chatOpen: current === 'chat_container',
_conference: conference,
_desktopSharingEnabled: desktopSharingEnabled,
_desktopSharingDisabledByConfig: disableDesktopSharing,
_desktopSharingDisabledTooltipKey: desktopSharingDisabledTooltipKey,
_dialog: Boolean(state['features/base/dialog'].component),
_editingDocument: Boolean(state['features/etherpad'].editing),
_etherpadInitialized: Boolean(state['features/etherpad'].initialized),
@@ -1149,11 +1231,13 @@ function _mapStateToProps(state) {
_hideInviteButton:
iAmRecorder || (!addPeopleEnabled && !dialOutEnabled),
_isGuest: state['features/base/jwt'].isGuest,
_fileRecordingsEnabled: isModerator && fileRecordingsEnabled,
_fileRecordingsDisabledTooltipKey: fileRecordingsDisabledTooltipKey,
_fileRecordingsEnabled: fileRecordingsEnabled,
_fileRecordingSession:
getActiveSession(state, JitsiRecordingConstants.mode.FILE),
_fullScreen: fullScreen,
_liveStreamingEnabled: isModerator && liveStreamingEnabled,
_liveStreamingDisabledTooltipKey: liveStreamingDisabledTooltipKey,
_liveStreamingEnabled: liveStreamingEnabled,
_liveStreamingSession:
getActiveSession(state, JitsiRecordingConstants.mode.STREAM),
_localParticipantID: localParticipant.id,