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",