From 2855ea1500d1a30e18ecc311c19c2c196f77cbac Mon Sep 17 00:00:00 2001 From: yanas Date: Fri, 19 May 2017 10:12:24 -0500 Subject: [PATCH] Adds dial-out UI. --- conference.js | 13 +- css/_dial-out.scss | 64 ++++ css/_flag-icon.scss | 35 ++ css/_variables.scss | 1 + css/components/_form-control.scss | 2 +- css/main.scss | 4 +- images/countries/au.svg | 9 + images/countries/ca.svg | 6 + images/countries/de.svg | 5 + images/countries/fr.svg | 7 + images/countries/gb.svg | 15 + images/countries/us.svg | 18 + interface_config.js | 2 +- lang/main.json | 17 +- modules/UI/UI.js | 19 +- react/features/dial-out/actionTypes.js | 48 +++ react/features/dial-out/actions.js | 97 +++++ .../dial-out/components/CountryIcon.js | 40 ++ .../dial-out/components/DialOutDialog.web.js | 226 ++++++++++++ .../components/DialOutNumbersForm.web.js | 346 ++++++++++++++++++ react/features/dial-out/components/index.js | 1 + react/features/dial-out/index.js | 4 + react/features/dial-out/reducer.js | 48 +++ .../invite/components/DialInNumbersForm.js | 6 +- react/features/toolbox/actions.web.js | 7 +- .../features/toolbox/defaultToolbarButtons.js | 64 +--- service/UI/UIEvents.js | 1 - 27 files changed, 1034 insertions(+), 71 deletions(-) create mode 100644 css/_dial-out.scss create mode 100755 css/_flag-icon.scss create mode 100755 images/countries/au.svg create mode 100755 images/countries/ca.svg create mode 100755 images/countries/de.svg create mode 100755 images/countries/fr.svg create mode 100755 images/countries/gb.svg create mode 100755 images/countries/us.svg create mode 100644 react/features/dial-out/actionTypes.js create mode 100644 react/features/dial-out/actions.js create mode 100644 react/features/dial-out/components/CountryIcon.js create mode 100644 react/features/dial-out/components/DialOutDialog.web.js create mode 100644 react/features/dial-out/components/DialOutNumbersForm.web.js create mode 100644 react/features/dial-out/components/index.js create mode 100644 react/features/dial-out/index.js create mode 100644 react/features/dial-out/reducer.js diff --git a/conference.js b/conference.js index cd8b0b3ac..d74f8ec05 100644 --- a/conference.js +++ b/conference.js @@ -1295,6 +1295,12 @@ export default { APP.UI.onSharedVideoStop(id); }); + room.on(ConferenceEvents.USER_STATUS_CHANGED, (id, status) => { + let user = room.getParticipantById(id); + if (user) { + APP.UI.updateUserStatus(user, status); + } + }); room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => { if (this.isLocalId(id)) { @@ -1651,13 +1657,6 @@ export default { }); }); - APP.UI.addListener(UIEvents.SIP_DIAL, (sipNumber) => { - room.dial(sipNumber) - .catch((err) => { - logger.error("Error dialing out", err); - }); - }); - APP.UI.addListener(UIEvents.RESOLUTION_CHANGED, (id, oldResolution, newResolution, delay) => { var logObject = { diff --git a/css/_dial-out.scss b/css/_dial-out.scss new file mode 100644 index 000000000..23fbb1547 --- /dev/null +++ b/css/_dial-out.scss @@ -0,0 +1,64 @@ +/** + * The dialog content element. + */ +.dial-out-content { + margin-top: 5px; + + /** + * The style of the flag icon. + */ + .dial-out-flag-icon { + position: absolute; + left: 5px; + top: 10px; + } + + /** + * The style of the dial code element. + */ + .dial-out-code { + padding-left: 25px !important; + } + + /** + * The dial-out dialog error element. + */ + .dial-out-error { + color: $errorColor; + } + + /** + * The style of the dial input element. + */ + .dial-out-input { + padding-left: 70px; + } + + /** + * Re-styling the default dropdown inside the dial-out-content. + */ + .dropdown { + left: $formPadding; + position: absolute !important; + width: 65px + } + + /** + * Re-styling the default form-control inside the dial-out-content. + */ + .form-control { + padding-bottom: 8px !important; + } + + .dropdown { + display: inline-block; + position: relative; + overflow: hidden; + } + + .dropdown-trigger-icon { + position: absolute; + right: 0; + top: 4px; + } +} diff --git a/css/_flag-icon.scss b/css/_flag-icon.scss new file mode 100755 index 000000000..aa6afd73d --- /dev/null +++ b/css/_flag-icon.scss @@ -0,0 +1,35 @@ +.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/_variables.scss b/css/_variables.scss index aeaaecf29..5473ca8b9 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -149,6 +149,7 @@ $inputControlEmColor: #f29424; //buttons $linkFontColor: #489afe; $linkHoverFontColor: #287ade; +$formPadding: 16px; /** * Unsupported browser diff --git a/css/components/_form-control.scss b/css/components/_form-control.scss index 227312162..d6e859fbb 100644 --- a/css/components/_form-control.scss +++ b/css/components/_form-control.scss @@ -1,5 +1,5 @@ .form-control { - padding: 16px 0; + padding: $formPadding 0; &:first-child { padding-top: 0; diff --git a/css/main.scss b/css/main.scss index bb77dedda..894620bb4 100644 --- a/css/main.scss +++ b/css/main.scss @@ -26,11 +26,13 @@ @import 'font'; @import 'font-awesome'; - /* Fonts END */ +@import 'flag-icon'; + /* Modules BEGIN */ +@import 'dial-out'; @import 'toastr'; @import 'base'; @import 'utils'; diff --git a/images/countries/au.svg b/images/countries/au.svg new file mode 100755 index 000000000..cd823e1e4 --- /dev/null +++ b/images/countries/au.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/countries/ca.svg b/images/countries/ca.svg new file mode 100755 index 000000000..fb542b029 --- /dev/null +++ b/images/countries/ca.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/countries/de.svg b/images/countries/de.svg new file mode 100755 index 000000000..344d6c938 --- /dev/null +++ b/images/countries/de.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/countries/fr.svg b/images/countries/fr.svg new file mode 100755 index 000000000..b17c8ad7c --- /dev/null +++ b/images/countries/fr.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/images/countries/gb.svg b/images/countries/gb.svg new file mode 100755 index 000000000..7296592e3 --- /dev/null +++ b/images/countries/gb.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/images/countries/us.svg b/images/countries/us.svg new file mode 100755 index 000000000..95e707b41 --- /dev/null +++ b/images/countries/us.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/interface_config.js b/interface_config.js index 5bd0545f4..56e44fd10 100644 --- a/interface_config.js +++ b/interface_config.js @@ -38,7 +38,7 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars //main toolbar 'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup', //extended toolbar - 'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'sip', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line + 'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line /** * Main Toolbar Buttons * All of them should be in TOOLBAR_BUTTONS diff --git a/lang/main.json b/lang/main.json index 8358bd233..75e67bcd9 100644 --- a/lang/main.json +++ b/lang/main.json @@ -277,8 +277,6 @@ "Save": "Save", "recording": "Recording", "recordingToken": "Enter recording token", - "Dial": "Dial", - "sipMsg": "Enter SIP number", "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", @@ -447,9 +445,16 @@ "unlocked": "This call is unlocked. Any new caller with the link may join the call." }, "videoStatus": { - "hd": "HD", - "hdVideo": "HD video", - "sd": "SD", - "sdVideo": "SD video" + "hd": "HD", + "hdVideo": "HD video", + "sd": "SD", + "sdVideo": "SD video" + }, + "dialOut": { + "dial": "Dial", + "dialOut": "Call a phone number", + "statusMessage": "is now __status__", + "enterPhone": "Enter phone number", + "phoneNotAllowed": "Oh, we don't support that destination yet! Sorry!" } } diff --git a/modules/UI/UI.js b/modules/UI/UI.js index ee5ce9e04..c404e2813 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -43,7 +43,7 @@ import { showDialPadButton, showEtherpadButton, showSharedVideoButton, - showSIPCallButton, + showDialOutButton, showToolbox } from '../../react/features/toolbox'; @@ -544,7 +544,7 @@ UI.onPeerVideoTypeChanged UI.updateLocalRole = isModerator => { VideoLayout.showModeratorIndicator(); - APP.store.dispatch(showSIPCallButton(isModerator)); + APP.store.dispatch(showDialOutButton(isModerator)); APP.store.dispatch(showSharedVideoButton()); Recording.showRecordingButton(isModerator); @@ -589,6 +589,21 @@ UI.updateUserRole = user => { } }; +/** + * Updates the user status. + * + * @param {JitsiParticipant} user - The user which status we need to update. + * @param {string} status - The new status. + */ +UI.updateUserStatus = (user, status) => { + let displayName = user.getDisplayName(); + messageHandler.notify( + displayName, '', 'connected', "dialOut.statusMessage", + { + status: UIUtil.escapeHtml(status) + }); +}; + /** * Toggles smileys in the chat. */ diff --git a/react/features/dial-out/actionTypes.js b/react/features/dial-out/actionTypes.js new file mode 100644 index 000000000..0f3b7c027 --- /dev/null +++ b/react/features/dial-out/actionTypes.js @@ -0,0 +1,48 @@ +import { Symbol } from '../base/react'; + +/** + * 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 new file mode 100644 index 000000000..d10ec18dd --- /dev/null +++ b/react/features/dial-out/actions.js @@ -0,0 +1,97 @@ +import { openDialog } from '../../features/base/dialog'; + +import { + DIAL_OUT_CANCELED, + DIAL_OUT_CODES_UPDATED, + DIAL_OUT_SERVICE_FAILED, + PHONE_NUMBER_CHECKED +} from './actionTypes'; + +import { DialOutDialog } from './components'; + +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) { + return (dispatch, getState) => { + 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) { + return (dispatch, getState) => { + const { dialOutAuthUrl } = getState()['features/base/config']; + + const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`; + + $.getJSON(fullUrl) + .success(response => + dispatch({ + type: PHONE_NUMBER_CHECKED, + response + })) + .error(error => + dispatch({ + type: DIAL_OUT_SERVICE_FAILED, + error + })); + }; +} + + +/** + * Opens the dial-out dialog. + * + * @returns {Function} + */ +export function openDialOutDialog() { + return openDialog(DialOutDialog); +} + +/** + * Sends an ajax request for dial-out country codes. + * + * @returns {Function} + */ +export function updateDialOutCodes() { + return (dispatch, getState) => { + const { dialOutCodesUrl } = getState()['features/base/config']; + + $.getJSON(dialOutCodesUrl) + .success(response => + dispatch({ + type: DIAL_OUT_CODES_UPDATED, + response + })) + .error(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 new file mode 100644 index 000000000..231de0762 --- /dev/null +++ b/react/features/dial-out/components/CountryIcon.js @@ -0,0 +1,40 @@ +import React, { Component } from 'react'; + +/** + * Implements a React Component to render a country flag icon. + */ +class CountryIcon extends Component { + /** + * {@code CountryIcon}'s property types. + * + * @static + */ + static propTypes = { + /** + * The css style class name. + */ + className: React.PropTypes.string, + + /** + * The 2-letter country code. + */ + countryCode: React.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 ; + + } +} + +export default CountryIcon; diff --git a/react/features/dial-out/components/DialOutDialog.web.js b/react/features/dial-out/components/DialOutDialog.web.js new file mode 100644 index 000000000..dbd09aec5 --- /dev/null +++ b/react/features/dial-out/components/DialOutDialog.web.js @@ -0,0 +1,226 @@ +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 Component which allows the user to dial out from the + * conference. + */ +class DialOutDialog extends Component { + + /** + * {@code DialOutDialog} component's property types. + * + * @static + */ + static propTypes = { + /** + * Property indicating if a dial number is allowed. + */ + _isDialNumberAllowed: React.PropTypes.bool, + + /** + * The function performing the cancel action. + */ + cancel: React.PropTypes.func, + + /** + * The function performing the phone number validity check. + */ + checkDialNumber: React.PropTypes.func, + + /** + * The function performing the dial action. + */ + dial: React.PropTypes.func, + + /** + * Invoked to obtain translated strings. + */ + t: React.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 {XML} + * @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 {XML} + * @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) { + // We remove all starting zeros from the dial input before attaching it + // to the country code. + const formattedDialInput = dialInput.replace(/^(0+)/, ''); + + const dialNumber = `${dialCode}${formattedDialInput}`; + + const formattedNumber = this._formatDialNumber(dialNumber); + + this.props.checkDialNumber(formattedNumber); + + 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: React.PropTypes.bool + * }} + */ +function _mapStateToProps(state) { + const { isDialNumberAllowed } = state['features/dial-out']; + + return { + /** + * Property indicating if a dial number is allowed. + * + * @private + * @type {boolean} + */ + _isDialNumberAllowed: isDialNumberAllowed + }; +} + +export default translate( + connect(_mapStateToProps, { + cancel, + dial, + checkDialNumber + })(DialOutDialog)); diff --git a/react/features/dial-out/components/DialOutNumbersForm.web.js b/react/features/dial-out/components/DialOutNumbersForm.web.js new file mode 100644 index 000000000..881674bc6 --- /dev/null +++ b/react/features/dial-out/components/DialOutNumbersForm.web.js @@ -0,0 +1,346 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import ExpandIcon from '@atlaskit/icon/glyph/expand'; +import { StatelessDropdownMenu } from '@atlaskit/dropdown-menu'; + +import { translate } from '../../base/i18n'; +import CountryIcon from './CountryIcon'; +import { updateDialOutCodes } from '../actions'; + +/** + * The expand icon of the dropdown menu. + * + * @type {XML} + */ +const EXPAND_ICON = ; + +/** + * The default value of the country if the fetch service is unavailable. + * + * @type {{name: string, dialCode: string, code: string}} + */ +const DEFAULT_COUNTRY = { + name: 'United States', + dialCode: '+1', + code: 'US' +}; + +/** + * 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: React.PropTypes.array, + + /** + * The function called on every dial input change. + */ + onChange: React.PropTypes.func, + + /** + * Invoked to obtain translated strings. + */ + t: React.PropTypes.func, + + /** + * Invoked to send an ajax request for dial-out codes. + */ + updateDialOutCodes: React.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._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; + + const items + = _dialOutCodes ? this._formatCountryCodes(_dialOutCodes) : []; + + return ( +
+ { this._createDropdownMenu(items) } +
+ +
+
+ ); + } + + /** + * Creates a {@code StatelessDropdownMenu} 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 ( +
+ + + + { EXPAND_ICON } + +
+ ); + } + + /** + * Transforms the passed in numbers object into an array of objects that can + * be parsed by {@code StatelessDropdownMenu}. + * + * @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}`, + elemBefore: countryElement, + country + }; + }); + } + + /** + * 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); + } + + /** + * 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: React.PropTypes.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 new file mode 100644 index 000000000..75776a0ae --- /dev/null +++ b/react/features/dial-out/components/index.js @@ -0,0 +1 @@ +export { default as DialOutDialog } from './DialOutDialog'; diff --git a/react/features/dial-out/index.js b/react/features/dial-out/index.js new file mode 100644 index 000000000..582e1f9dd --- /dev/null +++ b/react/features/dial-out/index.js @@ -0,0 +1,4 @@ +export * from './actions'; +export * from './components'; + +import './reducer'; diff --git a/react/features/dial-out/reducer.js b/react/features/dial-out/reducer.js new file mode 100644 index 000000000..e527b8799 --- /dev/null +++ b/react/features/dial-out/reducer.js @@ -0,0 +1,48 @@ +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: { + return DEFAULT_STATE; + } + 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/invite/components/DialInNumbersForm.js b/react/features/invite/components/DialInNumbersForm.js index ab9001b74..48ad4b3ba 100644 --- a/react/features/invite/components/DialInNumbersForm.js +++ b/react/features/invite/components/DialInNumbersForm.js @@ -192,7 +192,7 @@ class DialInNumbersForm extends Component { } /** - * Creates a React {@code Component} with a redonly HTMLInputElement as a + * 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. * @@ -269,7 +269,7 @@ class DialInNumbersForm extends Component { return []; } - const formattedNumbeers = phoneRegions.map(region => { + const formattedNumbers = phoneRegions.map(region => { const numbers = dialInNumbers[region]; return numbers.map(number => { @@ -280,7 +280,7 @@ class DialInNumbersForm extends Component { }); }); - return Array.prototype.concat(...formattedNumbeers); + return Array.prototype.concat(...formattedNumbers); } /** diff --git a/react/features/toolbox/actions.web.js b/react/features/toolbox/actions.web.js index 9396f1949..b7de47faa 100644 --- a/react/features/toolbox/actions.web.js +++ b/react/features/toolbox/actions.web.js @@ -215,14 +215,15 @@ export function showSharedVideoButton(): Function { } /** - * Shows SIP call button if it's required and appropriate flag is passed. + * Shows the dial out button if it's required and appropriate + * flag is passed. * * @param {boolean} show - Flag showing whether to show button or not. * @returns {Function} */ -export function showSIPCallButton(show: boolean): Function { +export function showDialOutButton(show: boolean): Function { return (dispatch: Dispatch<*>, getState: Function) => { - const buttonName = 'sip'; + const buttonName = 'dialout'; if (show && APP.conference.sipGatewayEnabled() diff --git a/react/features/toolbox/defaultToolbarButtons.js b/react/features/toolbox/defaultToolbarButtons.js index b7a6015d6..019a38547 100644 --- a/react/features/toolbox/defaultToolbarButtons.js +++ b/react/features/toolbox/defaultToolbarButtons.js @@ -5,39 +5,11 @@ import React from 'react'; import UIEvents from '../../../service/UI/UIEvents'; import { openInviteDialog } from '../invite'; +import { openDialOutDialog } from '../dial-out'; declare var APP: Object; -declare var config: Object; declare var JitsiMeetJS: Object; -/** - * Shows SIP number dialog. - * - * @returns {void} - */ -function _showSIPNumberInput() { - const defaultNumber = config.defaultSipNumber || ''; - const msgString - = ``; - - APP.UI.messageHandler.openTwoButtonDialog({ - focus: ':input:first', - leftButtonKey: 'dialog.Dial', - msgString, - titleKey: 'dialog.sipMsg', - - // eslint-disable-next-line max-params - submitFunction(event, value, message, formValues) { - const { sipNumber } = formValues; - - if (value && sipNumber) { - APP.UI.emitEvent(UIEvents.SIP_DIAL, sipNumber); - } - } - }); -} - /** * All toolbar buttons' descriptors. */ @@ -169,6 +141,23 @@ export default { tooltipKey: 'toolbar.sharescreen' }, + /** + * The descriptor of the dial out toolbar button. + */ + dialout: { + classNames: [ 'button', 'icon-telephone' ], + enabled: true, + + // Will be displayed once the SIP calls functionality is detected. + hidden: true, + id: 'toolbar_button_dial_out', + onClick() { + JitsiMeetJS.analytics.sendEvent('toolbar.sip.clicked'); + APP.store.dispatch(openDialOutDialog()); + }, + tooltipKey: 'dialOut.dialOut' + }, + /** * The descriptor of the dialpad toolbar button. */ @@ -395,22 +384,5 @@ export default { } ], tooltipKey: 'toolbar.sharedvideo' - }, - - /** - * The descriptor of the SIP call toolbar button. - */ - sip: { - classNames: [ 'button', 'icon-telephone' ], - enabled: true, - - // Will be displayed once the SIP calls functionality is detected. - hidden: true, - id: 'toolbar_button_sip', - onClick() { - JitsiMeetJS.analytics.sendEvent('toolbar.sip.clicked'); - _showSIPNumberInput(); - }, - tooltipKey: 'toolbar.sip' } }; diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 6de938bad..6674e6800 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -71,7 +71,6 @@ export default { HANGUP: "UI.hangup", LOGOUT: "UI.logout", RECORDING_TOGGLED: "UI.recording_toggled", - SIP_DIAL: "UI.sip_dial", SUBJECT_CHANGED: "UI.subject_changed", VIDEO_DEVICE_CHANGED: "UI.video_device_changed", AUDIO_DEVICE_CHANGED: "UI.audio_device_changed",