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/_font.scss b/css/_font.scss index 3522f50a0..97ab37db8 100644 --- a/css/_font.scss +++ b/css/_font.scss @@ -52,9 +52,6 @@ .icon-share-doc:before { content: "\e908"; } -.icon-telephone:before { - content: "\e909"; -} .icon-kick:before { content: "\e904"; } @@ -148,3 +145,6 @@ .icon-visibility-off:before { content: "\e924"; } +.icon-telephone:before { + content: "\e0cd"; +} 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/fonts/jitsi.eot b/fonts/jitsi.eot index abce56b5f..df7bcb832 100755 Binary files a/fonts/jitsi.eot and b/fonts/jitsi.eot differ diff --git a/fonts/jitsi.svg b/fonts/jitsi.svg index e5d2f7728..2c0e72f23 100755 --- a/fonts/jitsi.svg +++ b/fonts/jitsi.svg @@ -7,6 +7,7 @@ + @@ -20,7 +21,6 @@ - diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf index 06d7b3b82..603a81bc6 100755 Binary files a/fonts/jitsi.ttf and b/fonts/jitsi.ttf differ diff --git a/fonts/jitsi.woff b/fonts/jitsi.woff index ef4c204fa..940a303bb 100755 Binary files a/fonts/jitsi.woff and b/fonts/jitsi.woff differ diff --git a/fonts/selection.json b/fonts/selection.json index 52512aec2..0270035ba 100755 --- a/fonts/selection.json +++ b/fonts/selection.json @@ -27,7 +27,7 @@ "code": 59651 }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 0 }, { @@ -56,7 +56,7 @@ "code": 59677 }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 1 }, { @@ -85,7 +85,7 @@ "code": 59676 }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 2 }, { @@ -111,7 +111,7 @@ "name": "avatar" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 3 }, { @@ -137,7 +137,7 @@ "name": "hangup" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 4 }, { @@ -163,7 +163,7 @@ "name": "chat" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 5 }, { @@ -189,7 +189,7 @@ "name": "download" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 6 }, { @@ -215,7 +215,7 @@ "name": "edit" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 7 }, { @@ -241,35 +241,9 @@ "name": "share-doc" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 8 }, - { - "icon": { - "paths": [ - "M854 662c24 0 42 18 42 42v150c0 24-18 42-42 42-400 0-726-326-726-726 0-24 18-42 42-42h150c24 0 42 18 42 42 0 54 8 104 24 152 4 14 2 32-10 44l-94 94c62 122 162 220 282 282l94-94c12-12 30-14 44-10 48 16 98 24 152 24zM854 214v-44h-44v44h44zM768 128h128v128h-86v86h-42v-214zM640 214v128h-128v-44h86v-42h-86v-128h128v42h-86v44h86zM726 128v214h-44v-214h44z" - ], - "attrs": [], - "isMulticolor": false, - "isMulticolor2": false, - "tags": [ - "dialer_sip" - ], - "grid": 0 - }, - "attrs": [], - "properties": { - "id": 9, - "order": 95, - "ligatures": "dialer_sip", - "prevSize": 32, - "code": 59657, - "name": "telephone" - }, - "setIdx": 0, - "setId": 3, - "iconIdx": 9 - }, { "icon": { "paths": [ @@ -293,7 +267,7 @@ "name": "kick" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 10 }, { @@ -319,7 +293,7 @@ "name": "menu-up" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 11 }, { @@ -345,7 +319,7 @@ "name": "menu-down" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 12 }, { @@ -371,7 +345,7 @@ "name": "full-screen" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 13 }, { @@ -397,7 +371,7 @@ "name": "exit-full-screen" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 14 }, { @@ -423,7 +397,7 @@ "name": "star-full" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 15 }, { @@ -449,7 +423,7 @@ "name": "security" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 16 }, { @@ -475,7 +449,7 @@ "name": "security-locked" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 17 }, { @@ -501,7 +475,7 @@ "name": "reload" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 18 }, { @@ -527,7 +501,7 @@ "name": "microphone" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 19 }, { @@ -553,7 +527,7 @@ "name": "mic-empty" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 20 }, { @@ -579,7 +553,7 @@ "name": "mic-disabled" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 21 }, { @@ -605,7 +579,7 @@ "name": "raised-hand" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 22 }, { @@ -631,7 +605,7 @@ "name": "contactList" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 23 }, { @@ -657,7 +631,7 @@ "name": "link" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 24 }, { @@ -683,7 +657,7 @@ "name": "shared-video" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 25 }, { @@ -709,7 +683,7 @@ "name": "settings" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 26 }, { @@ -735,7 +709,7 @@ "name": "star" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 27 }, { @@ -761,7 +735,7 @@ "name": "switch-camera" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 28 }, { @@ -787,7 +761,7 @@ "name": "share-desktop" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 29 }, { @@ -813,7 +787,7 @@ "name": "camera" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 30 }, { @@ -839,7 +813,7 @@ "name": "camera-disabled" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 31 }, { @@ -865,7 +839,7 @@ "name": "volume" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 32 }, { @@ -913,7 +887,7 @@ "code": 59648 }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 33 }, { @@ -986,7 +960,7 @@ "ligatures": "" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 34 }, { @@ -1015,7 +989,7 @@ "ligatures": "" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 35 }, { @@ -1045,7 +1019,7 @@ "ligatures": "" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 36 }, { @@ -1075,7 +1049,7 @@ "ligatures": "" }, "setIdx": 0, - "setId": 3, + "setId": 1, "iconIdx": 37 }, { @@ -1095,14 +1069,14 @@ "properties": { "order": 115, "ligatures": "dialpad", - "id": 217, + "id": 38, "prevSize": 32, "code": 59685, "name": "dialpad" }, - "setIdx": 1, - "setId": 2, - "iconIdx": 217 + "setIdx": 0, + "setId": 1, + "iconIdx": 38 }, { "icon": { @@ -1121,14 +1095,14 @@ "properties": { "order": 114, "ligatures": "remove_red_eye, visibility", - "id": 622, + "id": 39, "prevSize": 32, "code": 59683, "name": "visibility" }, - "setIdx": 1, - "setId": 2, - "iconIdx": 622 + "setIdx": 0, + "setId": 1, + "iconIdx": 39 }, { "icon": { @@ -1147,14 +1121,41 @@ "properties": { "order": 113, "ligatures": "visibility_off", - "id": 816, + "id": 40, "prevSize": 32, "code": 59684, "name": "visibility-off" }, + "setIdx": 0, + "setId": 1, + "iconIdx": 40 + }, + { + "icon": { + "paths": [ + "M282 460c62 120 162 220 282 282l94-94c12-12 30-16 44-10 48 16 100 24 152 24 24 0 42 18 42 42v150c0 24-18 42-42 42-400 0-726-326-726-726 0-24 18-42 42-42h150c24 0 42 18 42 42 0 54 8 104 24 152 4 14 2 32-10 44z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "phone" + ], + "defaultCode": 57549, + "grid": 24 + }, + "attrs": [], + "properties": { + "ligatures": "call, local_phone, phone", + "id": 120, + "order": 848, + "prevSize": 24, + "code": 57549, + "name": "phone" + }, "setIdx": 1, - "setId": 2, - "iconIdx": 816 + "setId": 0, + "iconIdx": 120 } ], "height": 1024, @@ -1185,7 +1186,8 @@ "useClassSelector": true }, "historySize": 100, - "showCodes": true, - "search": "" + "showCodes": false, + "search": "", + "showLiga": false } } \ No newline at end of file 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/base/dialog/components/Dialog.web.js b/react/features/base/dialog/components/Dialog.web.js index 3800623f5..dd1b264fe 100644 --- a/react/features/base/dialog/components/Dialog.web.js +++ b/react/features/base/dialog/components/Dialog.web.js @@ -137,6 +137,7 @@ class Dialog extends AbstractDialog { appearance = 'primary' form = 'modal-dialog-form' id = 'modal-dialog-ok-button' + isDisabled = { this.props.okDisabled } onClick = { this._onSubmit }> { this.props.t(this.props.okTitleKey || 'dialog.Ok') } 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",