diff --git a/css/_dial-out.scss b/css/_dial-out.scss deleted file mode 100644 index 7cd7bbe2c..000000000 --- a/css/_dial-out.scss +++ /dev/null @@ -1,81 +0,0 @@ -/** - * The dialog content element. - */ -.dial-out-content { - margin-top: 5px; - - /** - * Wrap the contents in flex so items can be aligned on the same line. - */ - .form-control { - display: flex; - } - - /** - * The style of the flag icon. - */ - .dial-out-flag-icon { - position: absolute; - left: 5px; - top: 50%; - transform: translate(0, -50%); - } - - /** - * The style of the dial code element. - */ - .dial-out-code { - margin-bottom: 0; - padding-left: 25px; - } - - /** - * The dial-out dialog error element. - */ - .dial-out-error { - color: $errorColor; - } - - /** - * The style of the dial input element. - */ - .dial-out-input { - display: inline-block; - flex: 1; - margin-left: 5px; - } - - /** - * Re-styling the default dropdown inside the dial-out-content. - */ - .dropdown { - position: relative; - width: 65px; - } - - /** - * Re-styling the default form-control inside the dial-out-content. - */ - .form-control { - margin-bottom: 8px; - } - - .dropdown { - position: relative; - - input { - padding-left: 16px; - - &:read-only { - color: inherit; - } - } - } - - .dropdown-trigger-icon { - position: absolute; - right: 0; - top: 50%; - transform: translate(0, -50%); - } -} diff --git a/css/_flag-icon.scss b/css/_flag-icon.scss deleted file mode 100755 index aa6afd73d..000000000 --- a/css/_flag-icon.scss +++ /dev/null @@ -1,35 +0,0 @@ -.flag-icon-background { - background-size: contain; - background-position: 50%; - background-repeat: no-repeat; -} -.flag-icon { - background-size: contain; - background-position: 50%; - background-repeat: no-repeat; - position: relative; - display: inline-block; - width: 1.33333333em; - line-height: 1em; -} -.flag-icon:before { - content: "\00a0"; -} -.flag-icon-au { - background-image: url(../images/countries/au.svg); -} -.flag-icon-ca { - background-image: url(../images/countries/ca.svg); -} -.flag-icon-de { - background-image: url(../images/countries/de.svg); -} -.flag-icon-gb { - background-image: url(../images/countries/gb.svg); -} -.flag-icon-fr { - background-image: url(../images/countries/fr.svg); -} -.flag-icon-us { - background-image: url(../images/countries/us.svg); -} diff --git a/css/main.scss b/css/main.scss index 0b2650e11..ee7fe227d 100644 --- a/css/main.scss +++ b/css/main.scss @@ -28,11 +28,8 @@ @import 'font-awesome'; /* Fonts END */ -@import 'flag-icon'; - /* Modules BEGIN */ -@import 'dial-out'; @import 'aui_reset'; @import 'base'; @import 'utils'; diff --git a/css/modals/invite/_add-people.scss b/css/modals/invite/_add-people.scss index f1c62c408..06ed48768 100644 --- a/css/modals/invite/_add-people.scss +++ b/css/modals/invite/_add-people.scss @@ -11,17 +11,11 @@ padding-left: 5px; } } - } -} -/** - * Styles the loading element in the MultiSelectAutocomplete. - */ -.autocomplete-loading { - justify-content: center; - display: flex; - min-width: 260px; - padding: 20px; + .add-telephone-icon { + transform: scaleX(-1); + } + } } /** diff --git a/images/countries/au.svg b/images/countries/au.svg deleted file mode 100755 index cd823e1e4..000000000 --- a/images/countries/au.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/images/countries/ca.svg b/images/countries/ca.svg deleted file mode 100755 index fb542b029..000000000 --- a/images/countries/ca.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/images/countries/de.svg b/images/countries/de.svg deleted file mode 100755 index 344d6c938..000000000 --- a/images/countries/de.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/images/countries/fr.svg b/images/countries/fr.svg deleted file mode 100755 index b17c8ad7c..000000000 --- a/images/countries/fr.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/images/countries/gb.svg b/images/countries/gb.svg deleted file mode 100755 index 7296592e3..000000000 --- a/images/countries/gb.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/images/countries/us.svg b/images/countries/us.svg deleted file mode 100755 index 95e707b41..000000000 --- a/images/countries/us.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/lang/main.json b/lang/main.json index fdb588de9..f8957951b 100644 --- a/lang/main.json +++ b/lang/main.json @@ -452,17 +452,24 @@ "qualityButtonTip": "Change received video quality" }, "dialOut": { - "dial": "Dial", - "dialOut": "Call a number", - "statusMessage": "is now __status__", - "enterPhone": "Enter phone number", - "phoneNotAllowed": "Oh, we don't support that destination yet! Sorry!" + "statusMessage": "is now __status__" }, "addPeople": { "add": "Add", + "countryNotSupported": "We do not support this destination yet.", + "countryReminder": "Calling outside the US? Please make sure you start with the country code!", + "disabled": "You can't invite people.", + "invite": "Invite", + "loading": "Searching for people and phone numbers", + "loadingNumber": "Validating phone number", + "loadingPeople": "Searching for people to invite", "noResults": "No matching search results", - "searchPlaceholder": "Search for people and rooms to add", - "title": "Add people to your call", + "noValidNumbers": "Please enter a phone number", + "searchNumbers": "Enter a phone number to invite", + "searchPeople": "Enter a name to invite", + "searchPeopleAndNumbers": "Enter a name or phone number to invite", + "telephone": "Telephone: __number__", + "title": "Invite people to your meeting", "failedToAdd": "Failed to add members" }, "inlineDialogFailure": { diff --git a/react/features/base/react/components/web/MultiSelectAutocomplete.js b/react/features/base/react/components/web/MultiSelectAutocomplete.js index 2728b6a30..1312d38f6 100644 --- a/react/features/base/react/components/web/MultiSelectAutocomplete.js +++ b/react/features/base/react/components/web/MultiSelectAutocomplete.js @@ -1,6 +1,5 @@ import { MultiSelectStateless } from '@atlaskit/multi-select'; import AKInlineDialog from '@atlaskit/inline-dialog'; -import Spinner from '@atlaskit/spinner'; import _debounce from 'lodash/debounce'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; @@ -28,11 +27,22 @@ class MultiSelectAutocomplete extends Component { */ isDisabled: PropTypes.bool, + /** + * Text to display while a query is executing. + */ + loadingMessage: PropTypes.string, + /** * The text to show when no matches are found. */ noMatchesFound: PropTypes.string, + /** + * The function called immediately before a selection has been actually + * selected. Provides an opportunity to do any formatting. + */ + onItemSelected: PropTypes.func, + /** * The function called when the selection changes. */ @@ -113,14 +123,14 @@ class MultiSelectAutocomplete extends Component { } /** - * Clears the selected items. + * Sets the items to display as selected. * + * @param {Array} selectedItems - The list of items to display as + * having been selected. * @returns {void} */ - clear() { - this.setState({ - selectedItems: [] - }); + setSelectedItems(selectedItems = []) { + this.setState({ selectedItems }); } /** @@ -140,8 +150,10 @@ class MultiSelectAutocomplete extends Component { - { this._renderLoadingIndicator() } { this._renderError() } ); @@ -169,7 +180,8 @@ class MultiSelectAutocomplete extends Component { error: this.state.error && Boolean(filterValue), filterValue, isOpen: Boolean(this.state.items.length) && Boolean(filterValue), - items: filterValue ? this.state.items : [] + items: filterValue ? this.state.items : [], + loading: Boolean(filterValue) }); if (filterValue) { this._sendQuery(filterValue); @@ -201,7 +213,7 @@ class MultiSelectAutocomplete extends Component { if (existing) { selectedItems = selectedItems.filter(k => k !== existing); } else { - selectedItems.push(item); + selectedItems.push(this.props.onItemSelected(item)); } this.setState({ isOpen: false, @@ -236,33 +248,6 @@ class MultiSelectAutocomplete extends Component { ); } - /** - * Renders the loading indicator. - * - * @returns {ReactElement|null} - */ - _renderLoadingIndicator() { - if (!(this.state.loading - && !this.state.items.length - && this.state.filterValue.length)) { - return null; - } - - const content = ( // eslint-disable-line no-extra-parens -
- -
- ); - - return ( - - ); - } - /** * Sends a query to the resourceClient. * @@ -275,7 +260,6 @@ class MultiSelectAutocomplete extends Component { } this.setState({ - loading: true, error: false }); @@ -288,7 +272,6 @@ class MultiSelectAutocomplete extends Component { .then(results => { if (this.state.filterValue !== filterValue) { this.setState({ - loading: false, error: false }); diff --git a/react/features/dial-out/actionTypes.js b/react/features/dial-out/actionTypes.js deleted file mode 100644 index 5cd257faa..000000000 --- a/react/features/dial-out/actionTypes.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * The type of the action which signals a check for a dial-out phone number has - * succeeded. - * - * { - * type: PHONE_NUMBER_CHECKED, - * response: Object - * } - */ -export const PHONE_NUMBER_CHECKED - = Symbol('PHONE_NUMBER_CHECKED'); - -/** - * The type of the action which signals a cancel of the dial-out operation. - * - * { - * type: DIAL_OUT_CANCELED, - * response: Object - * } - */ -export const DIAL_OUT_CANCELED - = Symbol('DIAL_OUT_CANCELED'); - -/** - * The type of the action which signals a request for dial-out country codes has - * succeeded. - * - * { - * type: DIAL_OUT_CODES_UPDATED, - * response: Object - * } - */ -export const DIAL_OUT_CODES_UPDATED - = Symbol('DIAL_OUT_CODES_UPDATED'); - -/** - * The type of the action which signals a failure in some of dial-out service - * requests. - * - * { - * type: DIAL_OUT_SERVICE_FAILED, - * response: Object - * } - */ -export const DIAL_OUT_SERVICE_FAILED - = Symbol('DIAL_OUT_SERVICE_FAILED'); diff --git a/react/features/dial-out/actions.js b/react/features/dial-out/actions.js deleted file mode 100644 index aca7a42a9..000000000 --- a/react/features/dial-out/actions.js +++ /dev/null @@ -1,102 +0,0 @@ -// @flow - -import { - DIAL_OUT_CANCELED, - DIAL_OUT_CODES_UPDATED, - DIAL_OUT_SERVICE_FAILED, - PHONE_NUMBER_CHECKED -} from './actionTypes'; - -declare var $: Function; -declare var config: Object; - -/** - * Dials the given number. - * - * @returns {Function} - */ -export function cancel() { - return { - type: DIAL_OUT_CANCELED - }; -} - -/** - * Dials the given number. - * - * @param {string} dialNumber - The number to dial. - * @returns {Function} - */ -export function dial(dialNumber: string) { - return (dispatch: Dispatch<*>, getState: Function) => { - const { conference } = getState()['features/base/conference']; - - conference.dial(dialNumber); - }; -} - -/** - * Sends an ajax request for dial-out country codes. - * - * @param {string} dialNumber - The dial number to check for validity. - * @returns {Function} - */ -export function checkDialNumber(dialNumber: string) { - return (dispatch: Dispatch<*>, getState: Function) => { - const { dialOutAuthUrl } = getState()['features/base/config']; - - if (!dialOutAuthUrl) { - // no auth url, let's say it is valid - const response = {}; - - response.allow = true; - dispatch({ - type: PHONE_NUMBER_CHECKED, - response - }); - - return; - } - - const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`; - - $.getJSON(fullUrl) - .then(response => - dispatch({ - type: PHONE_NUMBER_CHECKED, - response - })) - .catch(error => - dispatch({ - type: DIAL_OUT_SERVICE_FAILED, - error - })); - }; -} - -/** - * Sends an ajax request for dial-out country codes. - * - * @returns {Function} - */ -export function updateDialOutCodes() { - return (dispatch: Dispatch<*>, getState: Function) => { - const { dialOutCodesUrl } = getState()['features/base/config']; - - if (!dialOutCodesUrl) { - return; - } - - $.getJSON(dialOutCodesUrl) - .then(response => - dispatch({ - type: DIAL_OUT_CODES_UPDATED, - response - })) - .catch(error => - dispatch({ - type: DIAL_OUT_SERVICE_FAILED, - error - })); - }; -} diff --git a/react/features/dial-out/components/CountryIcon.js b/react/features/dial-out/components/CountryIcon.js deleted file mode 100644 index 5fe453831..000000000 --- a/react/features/dial-out/components/CountryIcon.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; - -/** - * Implements a React {@link Component} to render a country flag icon. - */ -export default class CountryIcon extends Component { - /** - * {@code CountryIcon}'s property types. - * - * @static - */ - static propTypes = { - /** - * The css style class name. - */ - className: PropTypes.string, - - /** - * The 2-letter country code. - */ - countryCode: PropTypes.string - }; - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const iconClassName - = `flag-icon flag-icon-${ - this.props.countryCode} flag-icon-squared ${ - this.props.className}`; - - return ; - } -} diff --git a/react/features/dial-out/components/DialOutDialog.native.js b/react/features/dial-out/components/DialOutDialog.native.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/dial-out/components/DialOutDialog.web.js b/react/features/dial-out/components/DialOutDialog.web.js deleted file mode 100644 index 1fd31f9e6..000000000 --- a/react/features/dial-out/components/DialOutDialog.web.js +++ /dev/null @@ -1,248 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; - -import { translate } from '../../base/i18n'; -import { Dialog } from '../../base/dialog'; - -import { cancel, checkDialNumber, dial } from '../actions'; -import DialOutNumbersForm from './DialOutNumbersForm'; - -/** - * Implements a React {@link Component} which allows the user to dial out from - * the conference. - */ -class DialOutDialog extends Component { - /** - * {@code DialOutDialog} component's property types. - * - * @static - */ - static propTypes = { - /** - * The redux state representing the list of dial-out codes. - */ - _dialOutCodes: PropTypes.array, - - /** - * Property indicating if a dial number is allowed. - */ - _isDialNumberAllowed: PropTypes.bool, - - /** - * The function performing the cancel action. - */ - cancel: PropTypes.func, - - /** - * The function performing the phone number validity check. - */ - checkDialNumber: PropTypes.func, - - /** - * The function performing the dial action. - */ - dial: PropTypes.func, - - /** - * Invoked to obtain translated strings. - */ - t: PropTypes.func - }; - - /** - * Initializes a new {@code DialOutNumbersForm} instance. - * - * @param {Object} props - The read-only properties with which the new - * instance is to be initialized. - */ - constructor(props) { - super(props); - - this.state = { - /** - * The number to dial. - */ - dialNumber: '', - - /** - * Indicates if the dial input is currently empty. - */ - isDialInputEmpty: true - }; - - // Bind event handlers so they are only bound once for every instance. - this._onDialNumberChange = this._onDialNumberChange.bind(this); - this._onCancel = this._onCancel.bind(this); - this._onSubmit = this._onSubmit.bind(this); - } - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { _isDialNumberAllowed } = this.props; - - return ( - - { this._renderContent() } - - ); - } - - /** - * Formats the dial number in a way to remove all non digital characters - * from it (including spaces, brackets, dash, dot, etc.). - * - * @param {string} dialNumber - The phone number to format. - * @private - * @returns {string} - The formatted phone number. - */ - _formatDialNumber(dialNumber) { - return dialNumber.replace(/\D/g, ''); - } - - /** - * Renders the dialog content. - * - * @returns {ReactElement} - * @private - */ - _renderContent() { - const { _isDialNumberAllowed } = this.props; - - return ( -
- { _isDialNumberAllowed ? '' : this._renderErrorMessage() } - -
); - } - - /** - * Renders the error message to display if the dial phone number is not - * allowed. - * - * @returns {ReactElement} - * @private - */ - _renderErrorMessage() { - const { t } = this.props; - - return ( -
- { t('dialOut.phoneNotAllowed') } -
); - } - - /** - * Cancel the dial out. - * - * @private - * @returns {boolean} - Returns true to indicate that the dialog should be - * closed. - */ - _onCancel() { - this.props.cancel(); - - return true; - } - - /** - * Dials the number. - * - * @private - * @returns {boolean} - Returns true to indicate that the dialog should be - * closed. - */ - _onSubmit() { - if (this.props._isDialNumberAllowed) { - this.props.dial(this.state.dialNumber); - } - - return true; - } - - /** - * Updates the dialNumber and check for validity. - * - * @param {string} dialCode - The dial code value. - * @param {string} dialInput - The dial input value. - * @private - * @returns {void} - */ - _onDialNumberChange(dialCode, dialInput) { - let formattedDialInput, formattedNumber; - - // if there are no dial out codes it is possible they are disabled - // so we get the input as is, it can be just a sip address - if (this.props._dialOutCodes) { - // We remove all starting zeros from the dial input before attaching - // it to the country code. - formattedDialInput = dialInput.replace(/^(0+)/, ''); - - const dialNumber = `${dialCode}${formattedDialInput}`; - - formattedNumber = this._formatDialNumber(dialNumber); - - this.props.checkDialNumber(formattedNumber); - } else { - formattedNumber = formattedDialInput = dialInput; - } - - this.setState({ - dialNumber: formattedNumber, - isDialInputEmpty: !formattedDialInput - || formattedDialInput.length === 0 - }); - } -} - -/** - * Maps (parts of) the Redux state to the associated - * {@code DialOutDialog}'s props. - * - * @param {Object} state - The Redux state. - * @private - * @returns {{ - * _isDialNumberAllowed: boolean - * }} - */ -function _mapStateToProps(state) { - const { dialOutCodes, isDialNumberAllowed } = state['features/dial-out']; - - return { - /** - * List of dial-out codes. - * - * @private - * @type {array} - */ - _dialOutCodes: dialOutCodes, - - /** - * Property indicating if a dial number is allowed. - * - * @private - * @type {boolean} - */ - _isDialNumberAllowed: isDialNumberAllowed - }; -} - -export default translate( - connect(_mapStateToProps, { - cancel, - checkDialNumber, - dial - })(DialOutDialog)); diff --git a/react/features/dial-out/components/DialOutNumbersForm.web.js b/react/features/dial-out/components/DialOutNumbersForm.web.js deleted file mode 100644 index 6c6fee749..000000000 --- a/react/features/dial-out/components/DialOutNumbersForm.web.js +++ /dev/null @@ -1,369 +0,0 @@ -import { DropdownMenuStateless as DropdownMenu } from '@atlaskit/dropdown-menu'; -import { FieldTextStateless as TextField } from '@atlaskit/field-text'; -import ChevronDownIcon from '@atlaskit/icon/glyph/chevron-down'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; - -import { translate } from '../../base/i18n'; - -import { updateDialOutCodes } from '../actions'; -import CountryIcon from './CountryIcon'; - -/** - * The default value of the country if the fetch service is unavailable. - * - * @type {{ - * code: string, - * dialCode: string, - * name: string - * }} - */ -const DEFAULT_COUNTRY = { - code: 'US', - dialCode: '+1', - name: 'United States' -}; - -/** - * React {@code Component} responsible for fetching and displaying dial-out - * country codes, as well as dialing a phone number. - * - * @extends Component - */ -class DialOutNumbersForm extends Component { - /** - * {@code DialOutNumbersForm}'s property types. - * - * @static - */ - static propTypes = { - /** - * The redux state representing the list of dial-out codes. - */ - _dialOutCodes: PropTypes.array, - - /** - * The function called on every dial input change. - */ - onChange: PropTypes.func, - - /** - * Invoked to obtain translated strings. - */ - t: PropTypes.func, - - /** - * Invoked to send an ajax request for dial-out codes. - */ - updateDialOutCodes: PropTypes.func - }; - - /** - * Initializes a new {@code DialOutNumbersForm} instance. - * - * @param {Object} props - The read-only properties with which the new - * instance is to be initialized. - */ - constructor(props) { - super(props); - - this.state = { - dialInput: '', - - /** - * Whether or not the dropdown should be open. - * - * @type {boolean} - */ - isDropdownOpen: false, - - /** - * The selected country. - * - * @type {Object} - */ - selectedCountry: DEFAULT_COUNTRY - }; - - /** - * The internal reference to the DOM/HTML element backing the React - * {@code Component} text input. - * - * @private - * @type {HTMLInputElement} - */ - this._dialInputElem = null; - - // Bind event handlers so they are only bound once for every instance. - this._onDropdownTriggerInputChange - = this._onDropdownTriggerInputChange.bind(this); - this._onInputChange = this._onInputChange.bind(this); - this._onOpenChange = this._onOpenChange.bind(this); - this._onSelect = this._onSelect.bind(this); - this._setDialInputElement = this._setDialInputElement.bind(this); - } - - /** - * Dispatches a request for dial out codes if not already present in the - * redux store. If dial out codes are present, sets a default code to - * display in the dropdown trigger. - * - * @inheritdoc - * @returns {void} - */ - componentDidMount() { - const dialOutCodes = this.props._dialOutCodes; - - if (dialOutCodes) { - this._setDefaultCode(dialOutCodes); - } else { - this.props.updateDialOutCodes(); - } - } - - /** - * Monitors for dial out code updates and sets a default code to display in - * the dropdown trigger if not already set. - * - * @inheritdoc - * @returns {void} - */ - componentWillReceiveProps(nextProps) { - if (!this.state.selectedCountry && nextProps._dialOutCodes) { - this._setDefaultCode(nextProps._dialOutCodes); - } - } - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { t, _dialOutCodes } = this.props; - - return ( -
- { _dialOutCodes ? this._createDropdownMenu( - this._formatCountryCodes(_dialOutCodes)) : null } -
- -
-
- ); - } - - /** - * Creates a {@code DropdownMenu} instance. - * - * @param {Array} items - The content to display within the dropdown. - * @returns {ReactElement} - */ - _createDropdownMenu(items) { - const { code, dialCode } = this.state.selectedCountry; - - return ( -
- - { this._createDropdownTrigger(dialCode, code) } - -
- ); - } - - /** - * Creates a React {@code Component} with a readonly HTMLInputElement as a - * trigger for displaying the dropdown menu. The {@code Component} will also - * display the currently selected number. - * - * @param {string} dialCode - The +xx dial code. - * @param {string} countryCode - The country 2 letter code. - * @private - * @returns {ReactElement} - */ - _createDropdownTrigger(dialCode, countryCode) { - return ( -
- - { /** - * FIXME Replace TextField with AtlasKit Button when an issue - * with icons shrinking due to button text is fixed. - */ } - - - - -
- ); - } - - /** - * Transforms the passed in numbers object into an array of objects that can - * be parsed by {@code DropdownMenu}. - * - * @param {Object} countryCodes - The list of country codes. - * @private - * @returns {Array} - */ - _formatCountryCodes(countryCodes) { - return countryCodes.map(country => { - const countryIcon - = ; - const countryElement - = {countryIcon} { country.name }; - - return { - content: `${country.dialCode}`, - country, - elemBefore: countryElement - }; - }); - } - - /** - * Updates the dialNumber when changes to the dial text or code happen. - * - * @private - * @returns {void} - */ - _onDialNumberChange() { - const { dialCode } = this.state.selectedCountry; - - this.props.onChange(dialCode, this.state.dialInput); - } - - /** - * 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 DialOutNumbersForm} to - * get the desired AtlasKit input look for the UI. - * - * @returns {void} - */ - _onDropdownTriggerInputChange() { - // Intentionally left empty. - } - - /** - * Updates the dialInput state when the input changes. - * - * @param {Object} e - The event notifying us of the change. - * @private - * @returns {void} - */ - _onInputChange(e) { - this.setState({ - dialInput: e.target.value - }, () => { - this._onDialNumberChange(); - }); - } - - /** - * 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 country code. - * - * @param {Object} selection - Event from choosing an dropdown option. - * @private - * @returns {void} - */ - _onSelect(selection) { - this.setState({ - isDropdownOpen: false, - selectedCountry: selection.item.country - }, () => { - this._onDialNumberChange(); - - this._dialInputElem.focus(); - }); - } - - /** - * Updates the internal state of the currently selected number by defaulting - * to the first available number. - * - * @param {Object} countryCodes - The list of country codes to choose from - * for setting a default code. - * @private - * @returns {void} - */ - _setDefaultCode(countryCodes) { - this.setState({ - selectedCountry: countryCodes[0] - }); - } - - /** - * Sets the internal reference to the DOM/HTML element backing the React - * {@code Component} dial input. - * - * @param {HTMLInputElement} input - The DOM/HTML element for this - * {@code Component}'s text input. - * @private - * @returns {void} - */ - _setDialInputElement(input) { - this._dialInputElem = input; - } -} - -/** - * Maps (parts of) the Redux state to the associated - * {@code DialOutNumbersForm}'s props. - * - * @param {Object} state - The Redux state. - * @private - * @returns {{ - * _dialOutCodes: Object - * }} - */ -function _mapStateToProps(state) { - const { dialOutCodes } = state['features/dial-out']; - - return { - _dialOutCodes: dialOutCodes - }; -} - -export default translate( - connect(_mapStateToProps, { updateDialOutCodes })(DialOutNumbersForm)); diff --git a/react/features/dial-out/components/index.js b/react/features/dial-out/components/index.js deleted file mode 100644 index 75776a0ae..000000000 --- a/react/features/dial-out/components/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as DialOutDialog } from './DialOutDialog'; diff --git a/react/features/dial-out/index.js b/react/features/dial-out/index.js deleted file mode 100644 index 582e1f9dd..000000000 --- a/react/features/dial-out/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export * from './actions'; -export * from './components'; - -import './reducer'; diff --git a/react/features/dial-out/reducer.js b/react/features/dial-out/reducer.js deleted file mode 100644 index fcc1627f1..000000000 --- a/react/features/dial-out/reducer.js +++ /dev/null @@ -1,53 +0,0 @@ -import { - ReducerRegistry -} from '../base/redux'; - -import { - DIAL_OUT_CANCELED, - DIAL_OUT_CODES_UPDATED, - DIAL_OUT_SERVICE_FAILED, - PHONE_NUMBER_CHECKED -} from './actionTypes'; - -const DEFAULT_STATE = { - dialOutCodes: null, - error: null, - isDialNumberAllowed: true -}; - -ReducerRegistry.register( - 'features/dial-out', - (state = DEFAULT_STATE, action) => { - switch (action.type) { - case DIAL_OUT_CANCELED: { - // if we have already downloaded codes fill them in default state - // to skip another ajax query - return { - ...DEFAULT_STATE, - dialOutCodes: state.dialOutCodes - }; - } - case DIAL_OUT_CODES_UPDATED: { - return { - ...state, - error: null, - dialOutCodes: action.response - }; - } - case DIAL_OUT_SERVICE_FAILED: { - return { - ...state, - error: action.error - }; - } - case PHONE_NUMBER_CHECKED: { - return { - ...state, - error: null, - isDialNumberAllowed: action.response.allow - }; - } - } - - return state; - }); diff --git a/react/features/filmstrip/components/Filmstrip.web.js b/react/features/filmstrip/components/Filmstrip.web.js index ed6f823df..4a830ae05 100644 --- a/react/features/filmstrip/components/Filmstrip.web.js +++ b/react/features/filmstrip/components/Filmstrip.web.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; +import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants'; import { InviteButton } from '../../invite'; import { Toolbox } from '../../toolbox'; @@ -43,6 +44,18 @@ class Filmstrip extends Component<*> { */ _hovered: PropTypes.bool, + /** + * Whether or not the feature to directly invite people into the + * conference is available. + */ + _isAddToCallAvailable: PropTypes.bool, + + /** + * Whether or not the feature to dial out to number to join the + * conference is available. + */ + _isDialOutAvailable: PropTypes.bool, + /** * Whether or not the remote videos should be visible. Will toggle * a class for hiding the videos. @@ -93,6 +106,14 @@ class Filmstrip extends Component<*> { * @returns {ReactElement} */ render() { + const { + _hideInviteButton, + _isAddToCallAvailable, + _isDialOutAvailable, + _remoteVideosVisible, + filmstripOnly + } = this.props; + /** * Note: Appending of {@code RemoteVideo} views is handled through * VideoLayout. The views do not get blown away on render() because @@ -102,12 +123,12 @@ class Filmstrip extends Component<*> { * modified, then the views will get blown away. */ - const filmstripClassNames = `filmstrip ${this.props._remoteVideosVisible - ? '' : 'hide-videos'}`; + const filmstripClassNames = `filmstrip ${_remoteVideosVisible ? '' + : 'hide-videos'}`; return (
- { this.props.filmstripOnly ? : null } + { filmstripOnly ? : null }
@@ -116,9 +137,11 @@ class Filmstrip extends Component<*> { id = 'filmstripLocalVideo' onMouseOut = { this._onMouseOut } onMouseOver = { this._onMouseOver }> - { this.props.filmstripOnly - || this.props._hideInviteButton - ? null : } + { filmstripOnly || _hideInviteButton + ? null + : }
{ * @param {Object} state - The Redux state. * @private * @returns {{ - * _hovered: boolean, * _hideInviteButton: boolean, + * _hovered: boolean, + * _isAddToCallAvailable: boolean, + * _isDialOutAvailable: boolean, * _remoteVideosVisible: boolean * }} */ function _mapStateToProps(state) { + const { conference } = state['features/base/conference']; + const { + enableUserRolesBasedOnToken, + iAmRecorder + } = state['features/base/config']; + const { isGuest } = state['features/base/jwt']; + const { hovered } = state['features/filmstrip']; + + const isAddToCallAvailable = !isGuest; + const isDialOutAvailable + = getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR + && conference && conference.isSIPCallingSupported() + && (!enableUserRolesBasedOnToken || !isGuest); + return { - _hovered: state['features/filmstrip'].hovered, - _hideInviteButton: state['features/base/config'].iAmRecorder, + _hideInviteButton: iAmRecorder + || (!isAddToCallAvailable && !isDialOutAvailable), + _hovered: hovered, + _isAddToCallAvailable: isAddToCallAvailable, + _isDialOutAvailable: isDialOutAvailable, _remoteVideosVisible: shouldRemoteVideosBeVisible(state) }; } diff --git a/react/features/invite/components/AddPeopleDialog.web.js b/react/features/invite/components/AddPeopleDialog.web.js index cb4af975a..1da468247 100644 --- a/react/features/invite/components/AddPeopleDialog.web.js +++ b/react/features/invite/components/AddPeopleDialog.web.js @@ -11,12 +11,21 @@ import { getInviteURL } from '../../base/connection'; import { Dialog, hideDialog } from '../../base/dialog'; import { translate } from '../../base/i18n'; import { MultiSelectAutocomplete } from '../../base/react'; - -import { invitePeopleAndChatRooms, searchDirectory } from '../functions'; import { inviteVideoRooms } from '../../videosipgw'; +import { + checkDialNumber, + invitePeopleAndChatRooms, + searchDirectory +} from '../functions'; + +const logger = require('jitsi-meet-logger').getLogger(__filename); + declare var interfaceConfig: Object; +const isPhoneNumberRegex + = new RegExp(interfaceConfig.PHONE_NUMBER_REGEX || '^[0-9+()-\\s]*$'); + /** * The dialog that allows to invite people to the call. */ @@ -33,6 +42,11 @@ class AddPeopleDialog extends Component<*, *> { */ _conference: PropTypes.object, + /** + * The URL for validating if a phone number can be called. + */ + _dialOutAuthUrl: PropTypes.string, + /** * The URL pointing to the service allowing for people invite. */ @@ -58,6 +72,16 @@ class AddPeopleDialog extends Component<*, *> { */ _peopleSearchUrl: PropTypes.string, + /** + * Whether or not to show Add People functionality. + */ + enableAddPeople: PropTypes.bool, + + /** + * Whether or not to show Dial Out functionality. + */ + enableDialOut: PropTypes.bool, + /** * The function closing the dialog. */ @@ -76,33 +100,7 @@ class AddPeopleDialog extends Component<*, *> { _multiselect = null; - _resourceClient = { - makeQuery: text => { - const { - _jwt, - _peopleSearchQueryTypes, - _peopleSearchUrl - } = this.props; // eslint-disable-line no-invalid-this - - return ( - searchDirectory( - _peopleSearchUrl, - _jwt, - text, - _peopleSearchQueryTypes)); - }, - - parseResults: response => response.map(user => { - return { - content: user.name, - elemBefore: , - item: user, - value: user.id - }; - }) - }; + _resourceClient: Object; state = { /** @@ -116,6 +114,12 @@ class AddPeopleDialog extends Component<*, *> { */ addToCallInProgress: false, + + // FIXME: Remove usage of Immutable. {@code MultiSelectAutocomplete} + // will default to having its internal implementation use a plain array + // if no {@link defaultValue} is passed in. As such is the case, this + // instance of Immutable.List gets overridden with an array on the first + // search. /** * The list of invite items. */ @@ -133,9 +137,17 @@ class AddPeopleDialog extends Component<*, *> { // Bind event handlers so they are only bound once per instance. this._isAddDisabled = this._isAddDisabled.bind(this); + this._onItemSelected = this._onItemSelected.bind(this); this._onSelectionChange = this._onSelectionChange.bind(this); this._onSubmit = this._onSubmit.bind(this); + this._parseQueryResults = this._parseQueryResults.bind(this); + this._query = this._query.bind(this); this._setMultiSelectElement = this._setMultiSelectElement.bind(this); + + this._resourceClient = { + makeQuery: this._query, + parseResults: this._parseQueryResults + }; } /** @@ -153,7 +165,7 @@ class AddPeopleDialog extends Component<*, *> { && !this.state.addToCallInProgress && !this.state.addToCallError && this._multiselect) { - this._multiselect.clear(); + this._multiselect.setSelectedItems([]); } } @@ -163,18 +175,69 @@ class AddPeopleDialog extends Component<*, *> { * @returns {ReactElement} */ render() { + const { enableAddPeople, enableDialOut, t } = this.props; + let isMultiSelectDisabled = this.state.addToCallInProgress || false; + let placeholder; + let loadingMessage; + let noMatches; + + if (enableAddPeople && enableDialOut) { + loadingMessage = 'addPeople.loading'; + noMatches = 'addPeople.noResults'; + placeholder = 'addPeople.searchPeopleAndNumbers'; + } else if (enableAddPeople) { + loadingMessage = 'addPeople.loadingPeople'; + noMatches = 'addPeople.noResults'; + placeholder = 'addPeople.searchPeople'; + } else if (enableDialOut) { + loadingMessage = 'addPeople.loadingNumber'; + noMatches = 'addPeople.noValidNumbers'; + placeholder = 'addPeople.searchNumbers'; + } else { + isMultiSelectDisabled = true; + noMatches = 'addPeople.noResults'; + placeholder = 'addPeople.disabled'; + } + return ( - { this._renderUserInputForm() } + width = 'medium'> +
+ { this._renderErrorMessage() } + +
); } + _getDigitsOnly: (string) => string; + + /** + * Removes all non-numeric characters from a string. + * + * @param {string} text - The string from which to remove all characters + * except numbers. + * @private + * @returns {string} A string with only numbers. + */ + _getDigitsOnly(text = '') { + return text.replace(/\D/g, ''); + } + _isAddDisabled: () => boolean; /** @@ -189,6 +252,45 @@ class AddPeopleDialog extends Component<*, *> { || this.state.addToCallInProgress; } + _isMaybeAPhoneNumber: (string) => boolean; + + /** + * Checks whether a string looks like it could be for a phone number. + * + * @param {string} text - The text to check whether or not it could be a + * phone number. + * @private + * @returns {boolean} True if the string looks like it could be a phone + * number. + */ + _isMaybeAPhoneNumber(text) { + if (!isPhoneNumberRegex.test(text)) { + return false; + } + + const digits = this._getDigitsOnly(text); + + return Boolean(digits.length); + } + + _onItemSelected: (Object) => Object; + + /** + * Callback invoked when a selection has been made but before it has been + * set as selected. + * + * @param {Object} item - The item that has just been selected. + * @private + * @returns {Object} The item to display as selected in the input. + */ + _onItemSelected(item) { + if (item.item.type === 'phone') { + item.content = item.item.number; + } + + return item; + } + _onSelectionChange: (Map<*, *>) => void; /** @@ -199,55 +301,279 @@ class AddPeopleDialog extends Component<*, *> { * @returns {void} */ _onSelectionChange(selectedItems) { - const selectedIds = selectedItems.map(o => o.item); - this.setState({ - inviteItems: selectedIds + inviteItems: selectedItems }); } _onSubmit: () => void; /** - * Handles the submit button action. + * Invite people and numbers to the conference. The logic works by inviting + * numbers, people/rooms, and videosipgw in parallel. All invitees are + * stored in an array. As each invite succeeds, the invitee is removed + * from the array. After all invites finish, close the modal if there are + * no invites left to send. If any are left, that means an invite failed + * and an error state should display. * * @private * @returns {void} */ _onSubmit() { - if (!this._isAddDisabled()) { - this.setState({ - addToCallInProgress: true + if (this._isAddDisabled()) { + return; + } + + this.setState({ + addToCallInProgress: true + }); + + let allInvitePromises = []; + let invitesLeftToSend = [ + ...this.state.inviteItems + ]; + + // First create all promises for dialing out. + if (this.props.enableDialOut && this.props._conference) { + const phoneNumbers = invitesLeftToSend.filter( + ({ item }) => item.type === 'phone'); + + // For each number, dial out. On success, remove the number from + // {@link invitesLeftToSend}. + const phoneInvitePromises = phoneNumbers.map(number => { + const numberToInvite = this._getDigitsOnly(number.item.number); + + return this.props._conference.dial(numberToInvite) + .then(() => { + invitesLeftToSend + = invitesLeftToSend.filter(invite => + invite !== number); + }) + .catch(error => logger.error( + 'Error inviting phone number:', error)); + }); - const vrooms = this.state.inviteItems.filter( - i => i.type === 'videosipgw'); + allInvitePromises = allInvitePromises.concat(phoneInvitePromises); + } + + if (this.props.enableAddPeople) { + const usersAndRooms = invitesLeftToSend.filter(i => + i.item.type === 'user' || i.item.type === 'room') + .map(i => i.item); + + if (usersAndRooms.length) { + // Send a request to invite all the rooms and users. On success, + // filter all rooms and users from {@link invitesLeftToSend}. + const peopleInvitePromise = invitePeopleAndChatRooms( + this.props._inviteServiceUrl, + this.props._inviteUrl, + this.props._jwt, + usersAndRooms) + .then(() => { + invitesLeftToSend = invitesLeftToSend.filter(i => + i.item.type !== 'user' && i.item.type !== 'room'); + }) + .catch(error => logger.error( + 'Error inviting people:', error)); + + allInvitePromises.push(peopleInvitePromise); + } + + // Sipgw calls are fire and forget. Invite them to the conference + // then immediately remove them from {@link invitesLeftToSend}. + const vrooms = invitesLeftToSend.filter(i => + i.item.type === 'videosipgw') + .map(i => i.item); this.props._conference && vrooms.length > 0 - && this.props.inviteVideoRooms(this.props._conference, vrooms); + && this.props.inviteVideoRooms( + this.props._conference, vrooms); - invitePeopleAndChatRooms( - this.props._inviteServiceUrl, - this.props._inviteUrl, - this.props._jwt, - this.state.inviteItems.filter( - i => i.type === 'user' || i.type === 'room')) - .then( - /* onFulfilled */ () => { - this.setState({ - addToCallInProgress: false - }); + invitesLeftToSend = invitesLeftToSend.filter(i => + i.item.type !== 'videosipgw'); + } + + Promise.all(allInvitePromises) + .then(() => { + // If any invites are left that means something failed to send + // so treat it as an error. + if (invitesLeftToSend.length) { + logger.error(`${invitesLeftToSend.length} invites failed`); - this.props.hideDialog(); - }, - /* onRejected */ () => { this.setState({ addToCallInProgress: false, addToCallError: true }); + + if (this._multiselect) { + this._multiselect.setSelectedItems(invitesLeftToSend); + } + + return; + } + + this.setState({ + addToCallInProgress: false }); + + this.props.hideDialog(); + }); + } + + _parseQueryResults: (Array, string) => Array; + + /** + * Processes results from requesting available numbers and people by munging + * each result into a format {@code MultiSelectAutocomplete} can use for + * display. + * + * @param {Array} response - The response object from the server for the + * query. + * @private + * @returns {Object[]} Configuration objects for items to display in the + * search autocomplete. + */ + _parseQueryResults(response = []) { + const { t } = this.props; + const users = response.filter(item => item.type !== 'phone'); + const userDisplayItems = users.map(user => { + return { + content: user.name, + elemBefore: , + item: user, + tag: { + elemBefore: + }, + value: user.id + }; + }); + + const numbers = response.filter(item => item.type === 'phone'); + const telephoneIcon = this._renderTelephoneIcon(); + + const numberDisplayItems = numbers.map(number => { + const numberNotAllowedMessage + = number.allowed ? '' : t('addPeople.countryNotSupported'); + const countryCodeReminder = number.showCountryCodeReminder + ? t('addPeople.countryReminder') : ''; + const description + = `${numberNotAllowedMessage} ${countryCodeReminder}`.trim(); + + return { + filterValues: [ + number.originalEntry, + number.number + ], + content: t('addPeople.telephone', { number: number.number }), + description, + isDisabled: !number.allowed, + elemBefore: telephoneIcon, + item: number, + tag: { + elemBefore: telephoneIcon + }, + value: number.number + }; + }); + + return [ + ...userDisplayItems, + ...numberDisplayItems + ]; + } + + _query: (string) => Promise>; + + /** + * Performs a people and phone number search request. + * + * @param {string} query - The search text. + * @private + * @returns {Promise} + */ + _query(query = '') { + const text = query.trim(); + const { + _dialOutAuthUrl, + _jwt, + _peopleSearchQueryTypes, + _peopleSearchUrl + } = this.props; + + let peopleSearchPromise; + + if (this.props.enableAddPeople) { + peopleSearchPromise = searchDirectory( + _peopleSearchUrl, + _jwt, + text, + _peopleSearchQueryTypes); + } else { + peopleSearchPromise = Promise.resolve([]); } + + + const hasCountryCode = text.startsWith('+'); + let phoneNumberPromise; + + if (this.props.enableDialOut && this._isMaybeAPhoneNumber(text)) { + let numberToVerify = text; + + // When the number to verify does not start with a +, we assume no + // proper country code has been entered. In such a case, prepend 1 + // for the country code. The service currently takes care of + // prepending the +. + if (!hasCountryCode && !text.startsWith('1')) { + numberToVerify = `1${numberToVerify}`; + } + + // The validation service works properly when the query is digits + // only so ensure only digits get sent. + numberToVerify = this._getDigitsOnly(numberToVerify); + + phoneNumberPromise + = checkDialNumber(numberToVerify, _dialOutAuthUrl); + } else { + phoneNumberPromise = Promise.resolve({}); + } + + return Promise.all([ peopleSearchPromise, phoneNumberPromise ]) + .then(([ peopleResults, phoneResults ]) => { + const results = [ + ...peopleResults + ]; + + /** + * This check for phone results is for the day the call to + * searching people might return phone results as well. When + * that day comes this check will make it so the server checks + * are honored and the local appending of the number is not + * done. The local appending of the phone number can then be + * cleaned up when convenient. + */ + const hasPhoneResult = peopleResults.find( + result => result.type === 'phone'); + + if (!hasPhoneResult + && typeof phoneResults.allow === 'boolean') { + results.push({ + allowed: phoneResults.allow, + country: phoneResults.country, + type: 'phone', + number: phoneResults.phone, + originalEntry: text, + showCountryCodeReminder: !hasCountryCode + }); + } + + return results; + }); } /** @@ -294,28 +620,16 @@ class AddPeopleDialog extends Component<*, *> { } /** - * Renders the input form. + * Renders a telephone icon. * * @private * @returns {ReactElement} */ - _renderUserInputForm() { - const { t } = this.props; - + _renderTelephoneIcon() { return ( -
- { this._renderErrorMessage() } - -
+ + + ); } @@ -341,13 +655,19 @@ class AddPeopleDialog extends Component<*, *> { * @param {Object} state - The Redux state. * @private * @returns {{ + * _conference: Object, + * _dialOutAuthUrl: string, + * _inviteServiceUrl: string, + * _inviteUrl: string, * _jwt: string, + * _peopleSearchQueryTypes: Array, * _peopleSearchUrl: string * }} */ function _mapStateToProps(state) { const { conference } = state['features/base/conference']; const { + dialOutAuthUrl, inviteServiceUrl, peopleSearchQueryTypes, peopleSearchUrl @@ -355,6 +675,7 @@ function _mapStateToProps(state) { return { _conference: conference, + _dialOutAuthUrl: dialOutAuthUrl, _inviteServiceUrl: inviteServiceUrl, _inviteUrl: getInviteURL(state), _jwt: state['features/base/jwt'].jwt, diff --git a/react/features/invite/components/InviteButton.web.js b/react/features/invite/components/InviteButton.web.js index c4ead0246..efbb54c27 100644 --- a/react/features/invite/components/InviteButton.web.js +++ b/react/features/invite/components/InviteButton.web.js @@ -1,21 +1,11 @@ -/* global interfaceConfig */ - import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import Button from '@atlaskit/button'; -import DropdownMenu from '@atlaskit/dropdown-menu'; - -import { translate } from '../../base/i18n'; -import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants'; import { openDialog } from '../../base/dialog'; +import { translate } from '../../base/i18n'; import { AddPeopleDialog } from '.'; -import { DialOutDialog } from '../../dial-out'; -import { isInviteOptionEnabled } from '../functions'; - -const DIAL_OUT_OPTION = 'dialout'; -const ADD_TO_CALL_OPTION = 'addtocall'; /** * The button that provides different invite options. @@ -27,20 +17,20 @@ class InviteButton extends Component { * @static */ static propTypes = { + /** + * Invoked to open {@code AddPeopleDialog}. + */ + dispatch: PropTypes.func, + /** * Indicates if the "Add to call" feature is available. */ - _isAddToCallAvailable: PropTypes.bool, + enableAddPeople: PropTypes.bool, /** * Indicates if the "Dial out" feature is available. */ - _isDialOutAvailable: PropTypes.bool, - - /** - * The function opening the dialog. - */ - openDialog: PropTypes.func, + enableDialOut: PropTypes.bool, /** * Invoked to obtain translated strings. @@ -57,26 +47,8 @@ class InviteButton extends Component { constructor(props) { super(props); - this._onInviteOptionSelected = this._onInviteOptionSelected.bind(this); - this._updateInviteItems = this._updateInviteItems.bind(this); - - this._updateInviteItems(this.props); - } - - /** - * Implements React's {@link Component#componentWillReceiveProps()}. - * - * @inheritdoc - * @param {Object} nextProps - The read-only props which this Component will - * receive. - * @returns {void} - */ - componentWillReceiveProps(nextProps) { - if (this.props._isDialOutAvailable !== nextProps._isDialOutAvailable - || this.props._isAddToCallAvailable - !== nextProps._isAddToCallAvailable) { - this._updateInviteItems(nextProps); - } + // Bind event handler so it is only bound once for every instance. + this._onClick = this._onClick.bind(this); } /** @@ -85,144 +57,31 @@ class InviteButton extends Component { * @returns {ReactElement} */ render() { - // 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; - return (
- { this.state.inviteOptions[0].items.length - ? - : null }
); } /** - * Handles selection of the invite options. + * Opens {@code AddPeopleDialog}. * - * @param { Object } option - The invite option that has been selected from - * the dropdown menu. * @private * @returns {void} */ - _onInviteOptionSelected(option) { - this.state.inviteOptions[0].items.forEach(item => { - if (item.content === option.item.content) { - item.action(); - } - }); - } - - /** - * Updates the invite items list depending on the availability of the - * features. - * - * @param {Object} props - The read-only properties of the component. - * @private - * @returns {void} - */ - _updateInviteItems(props) { - const { INVITE_OPTIONS = [] } = interfaceConfig; - const validOptions = INVITE_OPTIONS.filter(option => - (option === DIAL_OUT_OPTION && props._isDialOutAvailable) - || (option === ADD_TO_CALL_OPTION && props._isAddToCallAvailable)); - - /* eslint-disable array-callback-return */ - - const inviteItems = validOptions.map(option => { - switch (option) { - case DIAL_OUT_OPTION: - return { - content: this.props.t('dialOut.dialOut'), - action: () => this.props.openDialog(DialOutDialog) - }; - 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 configuration for how the invite button should display and - * behave on click. - */ - buttonOption, - - /** - * The list of invite options in the dropdown. - */ - inviteOptions: [ - { - items: dropdownOptions - } - ] - }; - - if (this.state) { - this.setState(nextState); - } else { - // eslint-disable-next-line react/no-direct-mutation-state - this.state = nextState; - } + _onClick() { + this.props.dispatch(openDialog(AddPeopleDialog, { + enableAddPeople: this.props.enableAddPeople, + enableDialOut: this.props.enableDialOut + })); } } -/** - * Maps (parts of) the Redux state to the associated {@code InviteButton}'s - * props. - * - * @param {Object} state - The Redux state. - * @private - * @returns {{ - * _isAddToCallAvailable: boolean, - * _isDialOutAvailable: boolean - * }} - */ -function _mapStateToProps(state) { - const { conference } = state['features/base/conference']; - const { enableUserRolesBasedOnToken } = state['features/base/config']; - const { isGuest } = state['features/base/jwt']; - - return { - _isAddToCallAvailable: - !isGuest && isInviteOptionEnabled(ADD_TO_CALL_OPTION), - _isDialOutAvailable: - getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR - && conference && conference.isSIPCallingSupported() - && isInviteOptionEnabled(DIAL_OUT_OPTION) - && (!enableUserRolesBasedOnToken || !isGuest) - }; -} - -export default translate(connect(_mapStateToProps, { openDialog })( - InviteButton)); +export default translate(connect()(InviteButton)); diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js index 3bb3b8d1e..4c4fac143 100644 --- a/react/features/invite/functions.js +++ b/react/features/invite/functions.js @@ -75,7 +75,7 @@ export function searchDirectory( // eslint-disable-line max-params jwt: string, text: string, queryTypes: Array = [ 'conferenceRooms', 'user', 'room' ] -): Promise { +): Promise> { const queryTypesString = JSON.stringify(queryTypes); return new Promise((resolve, reject) => { @@ -86,3 +86,31 @@ export function searchDirectory( // eslint-disable-line max-params .catch((jqxhr, textStatus, error) => reject(error)); }); } + +/** + * Sends an ajax request to check if the phone number can be called. + * + * @param {string} dialNumber - The dial number to check for validity. + * @param {string} dialOutAuthUrl - The endpoint to use for checking validity. + * @returns {Promise} - The promise created by the request. + */ +export function checkDialNumber( + dialNumber: string, dialOutAuthUrl: string): Promise { + if (!dialOutAuthUrl) { + // no auth url, let's say it is valid + const response = { + allow: true, + phone: dialNumber + }; + + return Promise.resolve(response); + } + + const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`; + + return new Promise((resolve, reject) => { + $.getJSON(fullUrl) + .then(resolve) + .catch(reject); + }); +}