ref(invite): remove InviteDialog (#2483)

* ref(invite): remove InviteDialog

InviteDialog functionality has been moved into InfoDialog.
The InviteButton has been temporarily hacked to show one
of its dropdown options instead as the button. Future
work will bring in a redesigned InviteModal that the button
will open.

* squash: filter invalid options and map valid options

* squash: update strings
This commit is contained in:
virtuacoplenny 2018-02-17 11:53:39 -08:00 committed by yanas
parent e2cf7a788d
commit e47802538e
19 changed files with 49 additions and 1407 deletions

View File

@ -13,11 +13,6 @@
.clickable {
cursor: pointer;
}
.icon-security,
.icon-security-locked {
font-size: 16px;
}
}
#contacts {

View File

@ -65,7 +65,6 @@
@import 'components/button-control';
@import 'components/input-control';
@import 'components/input-slider';
@import "modals/invite/invite";
@import "connection-info";
@import 'aui-components/dropdown';
@import '404';

View File

@ -1,96 +0,0 @@
/*
* Sets the default cursor the remove password link. The link doesn't use
* the href attribute, so we need to set the cursor manually.
*/
#inviteDialogRemovePassword {
cursor: hand;
}
.invite-dialog {
.dial-in-numbers {
.dial-in-numbers-conference-id {
color: orange;
margin-left: 3px;
}
/*
* dial-in-numbers-copy styling is needed for the feature of copying
* text to the clipboard. The styling keeps the element invisible
* to the user but still programmatically selectable for copying.
*/
.dial-in-numbers-copy {
opacity: 0;
pointer-events: none;
position: fixed;
-webkit-user-select: text;
user-select: text;
}
.is-disabled,
.is-loading {
.dial-in-numbers-trigger-icon {
display: none;
}
}
}
.form-control {
padding: 0;
&__container {
/**
* Ensure contents display in a line and vertically centered.
*/
align-items: center;
button {
font-size: $modalButtonFontSize;
}
}
&__input-container {
flex: 1;
margin-right: 10px;
.dropdown-button-trigger {
text-align: left;
}
}
}
.inviteLink {
color: $readOnlyInputColor;
}
.lock-state {
display: flex;
}
.password-overview {
margin-top: 10px;
.form-control {
margin-top: 10px;
}
.password-overview-status,
.remove-password {
display: flex;
justify-content: space-between;
}
.password-overview-toggle-edit,
.remove-password-link {
cursor: pointer;
text-decoration: none;
}
.remove-password {
margin-top: 15px;
}
}
.remove-password-current {
color: $inputControlEmColor;
}
}

View File

@ -40,7 +40,7 @@ var interfaceConfig = {
TOOLBAR_BUTTONS: [
// main toolbar
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'fodeviceselection', 'hangup',
'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup',
// extended toolbar
'profile', 'contacts', 'info', 'chat', 'recording', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip' ],
@ -49,7 +49,7 @@ var interfaceConfig = {
* Main Toolbar Buttons
* All of them should be in TOOLBAR_BUTTONS
*/
MAIN_TOOLBAR_BUTTONS: [ 'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'fodeviceselection', 'hangup' ],
MAIN_TOOLBAR_BUTTONS: [ 'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup' ],
SETTINGS_SECTIONS: [ 'language', 'devices', 'moderator' ],
INVITE_OPTIONS: [ 'invite', 'dialout', 'addtocall' ],

View File

@ -103,7 +103,6 @@
"videomute": "Start / Stop camera",
"authenticate": "Authenticate",
"lock": "Lock / Unlock room",
"invite": "Share the link",
"chat": "Open / Close chat",
"etherpad": "Open / Close shared document",
"sharedvideo": "Share a YouTube video",
@ -228,7 +227,6 @@
"suboptimalExperienceDescription": "Eer... we are afraid your experience with __appName__ isn't going to be that great here. We are looking for ways to improve this but, until then, please try using one of the <a href='static/recommendedBrowsers.html' target='_blank'>fully supported browsers</a>."
},
"dialog": {
"add": "Add",
"allow": "Allow",
"kickMessage": "Ouch! You have been kicked out of the meet!",
"popupErrorTitle": "Pop-up blocked",
@ -243,7 +241,6 @@
"copy": "Copy",
"contactSupport": "Contact support",
"error": "Error",
"createPassword": "Create password",
"detectext": "Error when trying to detect desktopsharing extension.",
"failedpermissions": "Failed to obtain permissions to use the local microphone and/or camera.",
"conferenceReloadTitle": "Unfortunately, something went wrong.",
@ -294,10 +291,6 @@
"Save": "Save",
"recording": "Recording",
"recordingToken": "Enter recording token",
"passwordCheck": "Are you sure you would like to remove your password?",
"passwordMsg": "Set a password to lock your room",
"shareLink": "Share the link to the call",
"yourPassword": "Enter new password",
"Back": "Back",
"serviceUnavailable": "Service unavailable",
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
@ -468,19 +461,6 @@
"selectADevice": "Select a device",
"testAudio": "Test sound"
},
"invite": {
"addPassword": "Add password",
"callNumber": "Call __number__",
"enterID": "Enter Meeting ID: __conferenceID__ following by # to dial in from a phone",
"howToDialIn": "To dial in, use one of the following numbers and meeting ID",
"hidePassword": "Hide password",
"inviteTo": "Invite people to __conferenceName__",
"invitedYouTo": "__userName__ has invited you to the __inviteURL__ conference",
"invitePeople": "Invite",
"locked": "This call is locked. New callers must have the link and enter the password to join.",
"showPassword": "Show password",
"unlocked": "This call is unlocked. Any new caller with the link may join the call."
},
"videoStatus": {
"callQuality": "Call Quality",
"hd": "HD",
@ -500,7 +480,7 @@
},
"dialOut": {
"dial": "Dial",
"dialOut": "Call a #",
"dialOut": "Call a number",
"statusMessage": "is now __status__",
"enterPhone": "Enter phone number",
"phoneNotAllowed": "Oh, we don't support that destination yet! Sorry!"
@ -533,6 +513,7 @@
"veryGood": "Very Good"
},
"info": {
"addPassword": "Add password",
"cancelPassword": "Cancel password",
"conferenceURL": "Link: __url__",
"country": "Country",

View File

@ -1,25 +1,13 @@
// @flow
import { openDialog } from '../../features/base/dialog';
import {
SET_INFO_DIALOG_VISIBILITY,
UPDATE_DIAL_IN_NUMBERS_FAILED,
UPDATE_DIAL_IN_NUMBERS_SUCCESS
} from './actionTypes';
import { InviteDialog } from './components';
declare var $: Function;
/**
* Opens the Invite Dialog.
*
* @returns {Function}
*/
export function openInviteDialog() {
return openDialog(InviteDialog);
}
/**
* Opens the inline conference info dialog.
*

View File

@ -1,199 +0,0 @@
import Button from '@atlaskit/button';
import { FieldTextStateless as TextField } from '@atlaskit/field-text';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { setPassword } from '../../base/conference';
import { translate } from '../../base/i18n';
/**
* A React {@code Component} for locking a JitsiConference with a password.
*/
class AddPasswordForm extends Component {
/**
* {@code AddPasswordForm}'s property types.
*
* @static
*/
static propTypes = {
/**
* The JitsiConference on which to lock and set a password.
*
* @type {JitsiConference}
*/
conference: PropTypes.object,
/**
* Invoked to set a password on the conference.
*/
dispatch: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Initializes a new {@code AddPasswordForm} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
/**
* The current value to display in {@code AddPasswordForm}
* component's input field. The value is also used as the desired
* new password when creating a {@code setPassword} action.
*
* @type {string}
*/
password: ''
};
/**
* The internal reference to the React {@code component} for entering a
* password.
*
* @private
* @type {ReactComponent}
*/
this._inputComponent = null;
// Bind event handlers so they are only bound once for every instance.
this._onKeyDown = this._onKeyDown.bind(this);
this._onPasswordChange = this._onPasswordChange.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._setInput = this._setInput.bind(this);
}
/**
* Directly bind a handler to the input element. This is done in order to
* intercept enter presses so any outer forms do not become submitted.
* Atlaskit Button does not expose a way to hook onto keydown events.
*
* @inheritdoc
*/
componentDidMount() {
this._inputComponent.input.onkeydown = this._onKeyDown;
}
/**
* Remove any handlers set directly on DOM elements.
*
* @inheritdoc
*/
componentWillUnmount() {
this._inputComponent.input.onkeydown = null;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
return (
<div
className = 'form-control'
onSubmit = { this._onSubmit } >
<div className = 'form-control__container'>
<div className = 'form-control__input-container'>
<TextField
autoFocus = { true }
compact = { true }
id = 'newPasswordInput'
isLabelHidden = { true }
label = 'Enter Password'
onChange = { this._onPasswordChange }
onKeyDown = { this._onKeyDown }
placeholder = { t('dialog.createPassword') }
ref = { this._setInput }
shouldFitContainer = { true }
type = 'text' />
</div>
<Button
id = 'addPasswordBtn'
isDisabled = { !this.state.password }
onClick = { this._onSubmit }
type = 'button'>
{ t('dialog.add') }
</Button>
</div>
</div>
);
}
/**
* Mimics form behavior by listening for enter key press and submitting the
* entered password.
*
* @param {Object} event - DOM Event for keydown.
* @private
* @returns {void}
*/
_onKeyDown(event) {
event.stopPropagation();
if (event.keyCode === /* Enter */ 13) {
this._onSubmit();
}
}
/**
* Updates the internal state of the entered password.
*
* @param {Object} event - DOM Event for value change.
* @private
* @returns {void}
*/
_onPasswordChange(event) {
this.setState({ password: event.target.value });
}
/**
* Dispatches a request to lock the conference with a password.
*
* @private
* @returns {void}
*/
_onSubmit() {
if (!this.state.password) {
return;
}
const { conference } = this.props;
this.props.dispatch(setPassword(
conference,
conference.lock,
this.state.password
));
this.setState({ password: '' });
}
/**
* Sets the instance variable for the React Component used for entering a
* password.
*
* @param {Object} inputComponent - The React Component for the input
* field.
* @private
* @returns {void}
*/
_setInput(inputComponent) {
if (inputComponent !== this._inputComponent) {
this._inputComponent = inputComponent;
}
}
}
export default translate(connect()(AddPasswordForm));

View File

@ -1,406 +0,0 @@
import Button from '@atlaskit/button';
import DropdownMenu, {
DropdownItem, DropdownItemGroup } from '@atlaskit/dropdown-menu';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { getLocalParticipant } from '../../base/participants';
import { updateDialInNumbers } from '../actions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* React {@code Component} responsible for fetching and displaying telephone
* numbers for dialing into a conference. Also supports copying a selected
* dial-in number to the clipboard.
*
* @extends Component
*/
class DialInNumbersForm extends Component {
/**
* {@code DialInNumbersForm}'s property types.
*
* @static
*/
static propTypes = {
/**
* The redux state representing the dial-in numbers feature.
*/
_dialIn: PropTypes.object,
/**
* The display name of the local user.
*/
_localUserDisplayName: PropTypes.string,
/**
* Invoked to send an ajax request for dial-in numbers.
*/
dispatch: PropTypes.func,
/**
* The URL of the conference into which this {@code DialInNumbersForm}
* is inviting the local participant.
*/
inviteURL: PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Initializes a new {@code DialInNumbersForm} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
/**
* Whether or not the dropdown should be open.
*
* @type {boolean}
*/
isDropdownOpen: false,
/**
* The dial-in number to display as currently selected in the
* dropdown. The value should be an object which has two key/value
* pairs, content and number. The value of "content" will display in
* the dropdown while the value of "number" is a substring of
* "content" which will be copied to clipboard.
*
* @type {object}
*/
selectedNumber: null
};
/**
* The internal reference to the DOM/HTML element backing the React
* {@code Component} text area. It is necessary for the implementation
* of copying to the clipboard.
*
* @private
* @type {HTMLTextAreaElement}
*/
this._copyElement = null;
// Bind event handlers so they are only bound once for every instance.
this._onCopyClick = this._onCopyClick.bind(this);
this._onOpenChange = this._onOpenChange.bind(this);
this._onSelect = this._onSelect.bind(this);
this._setCopyElement = this._setCopyElement.bind(this);
}
/**
* Sets a default number to display in the dropdown trigger.
*
* @inheritdoc
* returns {void}
*/
componentWillMount() {
const { numbers } = this.props._dialIn;
if (numbers) {
this._setDefaultNumber(numbers);
} else {
this.props.dispatch(updateDialInNumbers());
}
}
/**
* Monitors for number updates and sets a default number to display in the
* dropdown trigger if not already set.
*
* @inheritdoc
* returns {void}
*/
componentWillReceiveProps(nextProps) {
if (!this.state.selectedNumber && nextProps._dialIn.numbers) {
this._setDefaultNumber(nextProps._dialIn.numbers);
}
}
/**
* Implements React's {@link Component#render()}. Returns null if the
* component is not ready for display.
*
* @inheritdoc
* @returns {ReactElement|null}
*/
render() {
const { _dialIn, t } = this.props;
const { conferenceID, numbers, numbersEnabled } = _dialIn;
const { selectedNumber } = this.state;
if (!conferenceID || !numbers || !numbersEnabled || !selectedNumber) {
return null;
}
const items = this._renderDropdownItems(numbers);
return (
<div className = 'form-control dial-in-numbers'>
<label className = 'form-control__label'>
{ t('invite.howToDialIn') }
<span className = 'dial-in-numbers-conference-id'>
{ conferenceID }
</span>
</label>
<div className = 'form-control__container'>
<div className = 'form-control__input-container'>
{ this._createDropdownMenu(items, selectedNumber) }
</div>
<Button
appearance = 'default'
onClick = { this._onCopyClick }
type = 'button'>
{ t('dialog.copy') }
</Button>
</div>
<textarea
className = 'dial-in-numbers-copy'
readOnly = { true }
ref = { this._setCopyElement }
tabIndex = '-1'
value = { this._generateCopyText() } />
</div>
);
}
/**
* Creates a {@code DropdownMenu} instance.
*
* @param {Array} items - The content to display within the dropdown.
* @param {string} triggerText - The text to display within the
* trigger element.
* @returns {ReactElement}
*/
_createDropdownMenu(items, triggerText) {
return (
<DropdownMenu
isOpen = { this.state.isDropdownOpen }
onOpenChange = { this._onOpenChange }
shouldFitContainer = { true }
trigger = { triggerText || '' }
triggerButtonProps = {{
className: 'dropdown-button-trigger',
shouldFitContainer: true }}
triggerType = 'button'>
<DropdownItemGroup>
{ items }
</DropdownItemGroup>
</DropdownMenu>
);
}
/**
* Formats the region and number string.
*
* @param {string} region - The region string.
* @param {string} number - The number string.
* @returns {string} - The new formatted string.
* @private
*/
_formatRegionNumber(region, number) {
return `${region}: ${number}`;
}
/**
* Creates a message describing how to dial in to the conference.
*
* @private
* @returns {string}
*/
_generateCopyText() {
const { t } = this.props;
const welcome = t('invite.invitedYouTo', {
inviteURL: this.props.inviteURL,
userName: this.props._localUserDisplayName
});
const callNumber = t('invite.callNumber', {
number: this.state.selectedNumber
});
const stepOne = `1) ${callNumber}`;
const enterID = t('invite.enterID', {
conferenceID: this.props._dialIn.conferenceID
});
const stepTwo = `2) ${enterID}`;
return `${welcome}\n${stepOne}\n${stepTwo}`;
}
/**
* Copies part of the number displayed in the dropdown trigger into the
* clipboard. Only the value specified in selectedNumber.number, which
* should be a substring of the displayed value, will be copied.
*
* @private
* @returns {void}
*/
_onCopyClick() {
try {
this._copyElement.select();
document.execCommand('copy');
this._copyElement.blur();
} catch (err) {
logger.error('error when copying the text', err);
}
}
/**
* Sets the internal state to either open or close the dropdown. If the
* dropdown is disabled, the state will always be set to false.
*
* @param {Object} dropdownEvent - The even returned from clicking on the
* dropdown trigger.
* @private
* @returns {void}
*/
_onOpenChange(dropdownEvent) {
this.setState({
isDropdownOpen: dropdownEvent.isOpen
});
}
/**
* Updates the internal state of the currently selected number.
*
* @param {Object} selection - Event from choosing an dropdown option.
* @private
* @returns {void}
*/
_onSelect(selection) {
this.setState({
isDropdownOpen: false,
selectedNumber: selection
});
}
/**
* Renders a DropDownItem for the given id and text.
*
* @param {string} id - The key identifier of the DropdownItem.
* @param {string} text - The text to display in the dropdown item.
* @returns {React.Component}
* @private
*/
_renderDropDownItem(id, text) {
return (
/**
* Arrow functions are not allowed in props, but I leave this until
* I figure a better way to implement the same thing.
*/
/* eslint-disable react/jsx-no-bind */
<DropdownItem
key = { id }
onClick = { () => this._onSelect(text || id) }>
{ text }
</DropdownItem>
/* eslint-disable react/jsx-no-bind */
);
}
/**
* Detects whether the response from dialInNumbersUrl returned an array or
* an object with dial-in numbers and calls the appropriate method to
* transform the numbers into the format expected by
* {@code DropdownMenu}.
*
* @param {Array<string>|Object} dialInNumbers - The numbers returned from
* requesting dialInNumbersUrl.
* @private
* @returns {Array<Object>}
*/
_renderDropdownItems(dialInNumbers) {
if (Array.isArray(dialInNumbers)) {
return dialInNumbers.map(number =>
this._renderDropDownItem(number)
);
}
const phoneRegions = Object.keys(dialInNumbers);
if (!phoneRegions.length) {
return [];
}
const dropdownItems = phoneRegions.map(region => {
const numbers = dialInNumbers[region];
return numbers.map(number =>
this._renderDropDownItem(number,
this._formatRegionNumber(region, number))
);
});
return Array.prototype.concat(...dropdownItems);
}
/**
* Sets the internal reference to the DOM/HTML element backing the React
* {@code Component} text area.
*
* @param {HTMLTextAreaElement} element - The DOM/HTML element for this
* {@code Component}'s text area.
* @private
* @returns {void}
*/
_setCopyElement(element) {
this._copyElement = element;
}
/**
* Updates the internal state of the currently selected number by defaulting
* to the first available number.
*
* @param {Object} dialInNumbers - The array or object of numbers to parse.
* @private
* @returns {void}
*/
_setDefaultNumber(dialInNumbers) {
let number = '';
if (Array.isArray(dialInNumbers)) {
number = dialInNumbers[0];
} else if (Object.keys(dialInNumbers).length > 0) {
const region = Object.keys(dialInNumbers)[0];
number = this._formatRegionNumber(region, dialInNumbers[region]);
}
this.setState({
selectedNumber: number
});
}
}
/**
* Maps (parts of) the Redux state to the associated
* {@code DialInNumbersForm}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _dialIn: Object,
* _localUserDisplayName: string
* }}
*/
function _mapStateToProps(state) {
return {
_localUserDisplayName: getLocalParticipant(state).name,
_dialIn: state['features/invite']
};
}
export default translate(connect(_mapStateToProps)(DialInNumbersForm));

View File

@ -10,11 +10,10 @@ import { translate } from '../../base/i18n';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
import { openDialog } from '../../base/dialog';
import { AddPeopleDialog, InviteDialog } from '.';
import { AddPeopleDialog } from '.';
import { DialOutDialog } from '../../dial-out';
import { isInviteOptionEnabled, getInviteOptionPosition } from '../functions';
import { isInviteOptionEnabled } from '../functions';
const SHARE_LINK_OPTION = 'invite';
const DIAL_OUT_OPTION = 'dialout';
const ADD_TO_CALL_OPTION = 'addtocall';
@ -58,7 +57,6 @@ class InviteButton extends Component {
constructor(props) {
super(props);
this._onInviteClick = this._onInviteClick.bind(this);
this._onInviteOptionSelected = this._onInviteOptionSelected.bind(this);
this._updateInviteItems = this._updateInviteItems.bind(this);
@ -87,7 +85,13 @@ class InviteButton extends Component {
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
// HACK ALERT: Normally children should not be controlling their own
// visibility; parents should control that. However, this component is
// in a transitionary state while the Invite Dialog is being redone.
// This hack will go away when the Invite Dialog is back.
if (!this.state.buttonOption) {
return null;
}
const { VERTICAL_FILMSTRIP } = interfaceConfig;
@ -95,12 +99,12 @@ class InviteButton extends Component {
<div className = 'filmstrip__invite'>
<div className = 'invite-button-group'>
<Button
onClick = { this._onInviteClick }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this.state.buttonOption.action }
shouldFitContainer = { true }>
{ t('invite.invitePeople') }
{ this.state.buttonOption.content }
</Button>
{ this.props._isDialOutAvailable
|| this.props._isAddToCallAvailable
{ this.state.inviteOptions[0].items.length
? <DropdownMenu
items = { this.state.inviteOptions }
onItemActivated = { this._onInviteOptionSelected }
@ -115,16 +119,6 @@ class InviteButton extends Component {
);
}
/**
* Handles the click of the invite button.
*
* @private
* @returns {void}
*/
_onInviteClick() {
this.props.openDialog(InviteDialog);
}
/**
* Handles selection of the invite options.
*
@ -150,48 +144,46 @@ class InviteButton extends Component {
* @returns {void}
*/
_updateInviteItems(props) {
const { t } = this.props;
const { INVITE_OPTIONS = [] } = interfaceConfig;
const validOptions = INVITE_OPTIONS.filter(option =>
(option === DIAL_OUT_OPTION && props._isDialOutAvailable)
|| (option === ADD_TO_CALL_OPTION && props._isAddToCallAvailable));
const inviteItems = [];
/* eslint-disable array-callback-return */
inviteItems.splice(
getInviteOptionPosition(SHARE_LINK_OPTION),
0,
{
content: t('toolbar.invite'),
action: () => this.props.openDialog(InviteDialog)
}
);
if (props._isDialOutAvailable) {
inviteItems.splice(
getInviteOptionPosition(DIAL_OUT_OPTION),
0,
{
content: t('dialOut.dialOut'),
const inviteItems = validOptions.map(option => {
switch (option) {
case DIAL_OUT_OPTION:
return {
content: this.props.t('dialOut.dialOut'),
action: () => this.props.openDialog(DialOutDialog)
}
);
}
if (props._isAddToCallAvailable) {
inviteItems.splice(
getInviteOptionPosition(ADD_TO_CALL_OPTION),
0,
{
};
case ADD_TO_CALL_OPTION:
return {
content: interfaceConfig.ADD_PEOPLE_APP_NAME,
action: () => this.props.openDialog(AddPeopleDialog)
}
);
}
};
}
});
/* eslint-enable array-callback-return */
const buttonOption = inviteItems[0];
const dropdownOptions = inviteItems.splice(1, inviteItems.length);
const nextState = {
/**
* The list of invite options.
* The configuration for how the invite button should display and
* behave on click.
*/
buttonOption,
/**
* The list of invite options in the dropdown.
*/
inviteOptions: [
{
items: inviteItems
items: dropdownOptions
}
]
};

View File

@ -1,120 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
createInviteDialogClosedEvent,
sendAnalytics
} from '../../analytics';
import { getInviteURL } from '../../base/connection';
import { Dialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
import DialInNumbersForm from './DialInNumbersForm';
import PasswordContainer from './PasswordContainer';
import ShareLinkForm from './ShareLinkForm';
/**
* A React {@code Component} for displaying other components responsible for
* copying the current conference url and for setting or removing a conference
* password.
*/
class InviteDialog extends Component {
/**
* {@code InviteDialog} component's property types.
*
* @static
*/
static propTypes = {
/**
* Whether or not the current user can modify the current password.
*/
_canEditPassword: PropTypes.bool,
/**
* The redux store representation of the JitsiConference.
*/
_conference: PropTypes.object,
/**
* The url for the JitsiConference.
*/
_inviteURL: PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Reports an analytics event for the invite modal being closed.
*
* @inheritdoc
*/
componentWillUnmount() {
sendAnalytics(createInviteDialogClosedEvent());
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _canEditPassword, _conference, _inviteURL, t } = this.props;
const titleString
= t('invite.inviteTo', { conferenceName: _conference.room });
return (
<Dialog
cancelDisabled = { true }
okTitleKey = 'dialog.done'
titleString = { titleString }>
<div className = 'invite-dialog'>
<ShareLinkForm toCopy = { _inviteURL } />
<DialInNumbersForm inviteURL = { _inviteURL } />
<PasswordContainer
conference = { _conference.conference }
locked = { _conference.locked }
password = { _conference.password }
showPasswordEdit = { _canEditPassword } />
</div>
</Dialog>
);
}
}
/**
* Maps (parts of) the Redux state to the associated {@code InviteDialog}'s
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _canEditPassword: boolean,
* _conference: Object,
* _inviteURL: string
* }}
*/
function _mapStateToProps(state) {
const isModerator
= getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR;
let canEditPassword;
if (state['features/base/config'].enableUserRolesBasedOnToken) {
canEditPassword = isModerator && !state['features/base/jwt'].isGuest;
} else {
canEditPassword = isModerator;
}
return {
_canEditPassword: canEditPassword,
_conference: state['features/base/conference'],
_inviteURL: getInviteURL(state)
};
}
export default translate(connect(_mapStateToProps)(InviteDialog));

View File

@ -1,59 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
/**
* A React Component for displaying the conference lock state.
*/
class LockStatePanel extends Component {
/**
* {@code LockStatePanel}'s property types.
*
* @static
*/
static propTypes = {
/**
* Whether or not the conference is currently locked.
*/
locked: PropTypes.bool,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
let iconClass;
let stateClass;
let textKey;
if (this.props.locked) {
iconClass = 'icon-security-locked';
stateClass = 'is-locked';
textKey = 'invite.locked';
} else {
iconClass = 'icon-security';
stateClass = 'is-unlocked';
textKey = 'invite.unlocked';
}
return (
<div className = { `lock-state ${stateClass}` }>
<span className = { iconClass } />
<span>
{ this.props.t(textKey) }
</span>
</div>
);
}
}
export default translate(LockStatePanel);

View File

@ -1,157 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
import { LOCKED_LOCALLY } from '../../room-lock';
import AddPasswordForm from './AddPasswordForm';
import LockStatePanel from './LockStatePanel';
import RemovePasswordForm from './RemovePasswordForm';
/**
* React {@code Component} for displaying the current room lock state as well as
* exposing features to modify the room lock.
*/
class PasswordContainer extends Component {
/**
* {@code PasswordContainer}'s property types.
*
* @static
*/
static propTypes = {
/**
* The JitsiConference for which to display a lock state and change the
* password.
*
* @type {JitsiConference}
*/
conference: PropTypes.object,
/**
* The value for how the conference is locked (or undefined if not
* locked) as defined by room-lock constants.
*/
locked: PropTypes.string,
/**
* The current known password for the JitsiConference.
*/
password: PropTypes.string,
/**
* Whether or not the password editing components should be displayed.
*/
showPasswordEdit: PropTypes.bool,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Initializes a new {@code PasswordContainer} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
/**
* Whether or not the form to edit the password should display. If
* true, the form should display.
*
* @type {boolean}
*/
isEditingPassword: false
};
// Bind event handlers so they are only bound once for every instance.
this._onTogglePasswordEdit = this._onTogglePasswordEdit.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div className = 'password-overview'>
<div className = 'password-overview-status'>
<LockStatePanel locked = { Boolean(this.props.locked) } />
{ this._renderShowPasswordLink() }
</div>
{ this._renderPasswordEdit() }
</div>
);
}
/**
* Toggles the display of the ReactElements used to edit the password.
*
* @private
* @returns {void}
*/
_onTogglePasswordEdit() {
this.setState({
isEditingPassword: !this.state.isEditingPassword
});
}
/**
* Creates a ReactElement used for setting or removing a password.
*
* @private
* @returns {ReactElement|null}
*/
_renderPasswordEdit() {
if (!this.state.isEditingPassword) {
return null;
}
return (
this.props.locked
? <RemovePasswordForm
conference = { this.props.conference }
lockedLocally = { this.props.locked === LOCKED_LOCALLY }
password = { this.props.password } />
: <AddPasswordForm conference = { this.props.conference } />
);
}
/**
* Creates a ReactElement that toggles displaying password edit components.
*
* @private
* @returns {ReactElement|null}
*/
_renderShowPasswordLink() {
if (!this.props.showPasswordEdit) {
return null;
}
let toggleStatusKey;
if (this.state.isEditingPassword) {
toggleStatusKey = 'invite.hidePassword';
} else if (this.props.locked) {
toggleStatusKey = 'invite.showPassword';
} else {
toggleStatusKey = 'invite.addPassword';
}
return (
<a
className = 'password-overview-toggle-edit'
onClick = { this._onTogglePasswordEdit }>
{ this.props.t(toggleStatusKey) }
</a>
);
}
}
export default translate(PasswordContainer);

View File

@ -1,119 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { setPassword } from '../../base/conference';
import { translate } from '../../base/i18n';
/**
* A React {@code Component} for removing a lock from a JitsiConference.
*/
class RemovePasswordForm extends Component {
/**
* {@code RemovePasswordForm}'s property types.
*
* @static
*/
static propTypes = {
/**
* The JitsiConference on which remove a lock.
*
* @type {JitsiConference}
*/
conference: PropTypes.object,
/**
* Invoked to send a password removal request.
*/
dispatch: PropTypes.func,
/**
* Whether or not the room lock, if any, was set by the local user.
*/
lockedLocally: PropTypes.bool,
/**
* The current known password for the JitsiConference.
*/
password: PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Initializes a new {@code RemovePasswordForm} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onClick = this._onClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @private
* @returns {ReactElement}
*/
render() {
return (
<div className = 'remove-password'>
<div className = 'remove-password-description'>
{ this._getPasswordPreviewText() }
</div>
<a
className = 'remove-password-link'
id = 'inviteDialogRemovePassword'
onClick = { this._onClick }>
{ this.props.t('dialog.removePassword') }
</a>
</div>
);
}
/**
* Creates a ReactElement for displaying the current password.
*
* @private
* @returns {ReactElement}
*/
_getPasswordPreviewText() {
const { lockedLocally, password, t } = this.props;
return (
<span>
<span>
{ `${t('dialog.currentPassword')} ` }
</span>
<span className = 'remove-password-current'>
{ lockedLocally ? password : t('passwordSetRemotely') }
</span>
</span>
);
}
/**
* Dispatches a request to remove any set password on the JitsiConference.
*
* @private
* @returns {void}
*/
_onClick() {
const { conference } = this.props;
this.props.dispatch(setPassword(
conference,
conference.lock,
''
));
}
}
export default translate(connect()(RemovePasswordForm));

View File

@ -1,140 +0,0 @@
import Button from '@atlaskit/button';
import { FieldTextStateless as TextField } from '@atlaskit/field-text';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* A React {@code Component} for displaying a value with a copy button to copy
* the value into the clipboard.
*/
class ShareLinkForm extends Component {
/**
* {@code ShareLinkForm}'s property types.
*
* @static
*/
static propTypes = {
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func,
/**
* The value to be displayed and copied into the clipboard.
*/
toCopy: PropTypes.string
};
/**
* Initializes a new {@code ShareLinkForm} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
/**
* The internal reference to the React {@code component} for display
* the meeting link in an input element.
*
* @private
* @type {ReactComponent}
*/
this._inputComponent = null;
// Bind event handlers so they are only bound once for every instance.
this._onClick = this._onClick.bind(this);
this._onDropdownTriggerInputChange
= this._onDropdownTriggerInputChange.bind(this);
this._setInput = this._setInput.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const inputValue = this.props.toCopy || t('inviteUrlDefaultMsg');
return (
<div className = 'form-control'>
<label className = 'form-control__label'>
{ t('dialog.shareLink') }
</label>
<div className = 'form-control__container'>
<div className = 'form-control__input-container'>
<TextField
id = 'inviteLinkRef'
isLabelHidden = { true }
isReadOnly = { true }
label = 'invite link'
onChange = { this._onDropdownTriggerInputChange }
ref = { this._setInput }
shouldFitContainer = { true }
type = 'text'
value = { inputValue } />
</div>
<Button
appearance = 'default'
onClick = { this._onClick }
type = 'button'>
{ t('dialog.copy') }
</Button>
</div>
</div>
);
}
/**
* Copies the passed in value to the clipboard.
*
* @private
* @returns {void}
*/
_onClick() {
try {
const { input } = this._inputComponent;
input.select();
document.execCommand('copy');
input.blur();
} catch (err) {
logger.error('error when copying the text', err);
}
}
/**
* This is a no-op function used to stub out TextField's onChange in order
* to prevent TextField from printing prop type validation errors. TextField
* is used as a trigger for the dropdown in {@code ShareLinkForm} to get the
* desired AtlasKit input look for the UI.
*
* @returns {void}
*/
_onDropdownTriggerInputChange() {
// Intentionally left empty.
}
/**
* Sets the internal reference to the React Component wrapping the input
* with id {@code inviteLinkRef}.
*
* @param {ReactComponent} inputComponent - React Component for displaying
* an input for displaying the meeting link.
* @private
* @returns {void}
*/
_setInput(inputComponent) {
this._inputComponent = inputComponent;
}
}
export default translate(ShareLinkForm);

View File

@ -104,7 +104,7 @@ class DialInInfoPage extends Component {
if (loading) {
contents = '';
} else if (numbersEnabled === false) {
contents = this.props.t('invite.disabled');
contents = this.props.t('info.dialInNotSupported');
} else if (error) {
contents = error;
} else {

View File

@ -1,4 +1,3 @@
export { default as AddPeopleDialog } from './AddPeopleDialog';
export { default as InfoDialogButton } from './InfoDialogButton';
export { default as InviteButton } from './InviteButton';
export { default as InviteDialog } from './InviteDialog';

View File

@ -411,7 +411,7 @@ class InfoDialog extends Component {
} else {
className = 'add-password';
onClick = this._onTogglePasswordEditState;
textKey = 'invite.addPassword';
textKey = 'info.addPassword';
}
return className && onClick && textKey

View File

@ -12,7 +12,7 @@ import {
} from '../analytics';
import { ParticipantCounter } from '../contact-list';
import { openDeviceSelectionDialog } from '../device-selection';
import { InfoDialogButton, openInviteDialog } from '../invite';
import { InfoDialogButton } from '../invite';
import UIEvents from '../../../service/UI/UIEvents';
import { VideoQualityButton } from '../video-quality';
@ -307,22 +307,6 @@ export default function getDefaultButtons() {
component: InfoDialogButton
},
/**
* The descriptor of the toolbar button which shows the invite user
* dialog.
*/
invite: {
classNames: [ 'button', 'icon-link' ],
enabled: true,
id: 'toolbar_button_link',
onClick(dispatch: Function) {
sendAnalytics(createToolbarEvent('invite'));
dispatch(openInviteDialog());
},
tooltipKey: 'toolbar.invite'
},
/**
* The descriptor of the microphone toolbar button.
*/