Adds new format of phoneList service and re-design dial in numbers page. (#3903)

* Adds new format of phoneList service and re-design dial in numbers page.

Adds flags and country names (with translations) for the numbers if using the new format.

* Fixes tests and fixes get default number.

* Updates swagger with new format.

* Moves html back yo table.

Fixes displaying on mobile and also the tel: URI generation. The tel: URI is tested on Android and iOS and seems to work (Android was not interpreting 'p', but both seems to like ',').

* Fixes a wrong return statement.

* Small fixes.
This commit is contained in:
Дамян Минков 2019-02-26 13:32:46 +00:00 committed by GitHub
parent d7eea8abbc
commit ea4d49f2a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 351 additions and 138 deletions

View File

@ -28,7 +28,7 @@
max-width: 40em;
padding: 35px 0 40px 0;
text-align: center;
width: 75%;
width: 90%;
a:active {
text-decoration: none;
@ -46,7 +46,7 @@
&__text,
.deep-linking-dial-in {
font-size: 1.2em;
font-size: 1em;
line-height: em(29px, 21px);
margin-bottom: 0.65em;
@ -59,6 +59,26 @@
font-size: em(21, 18);
}
}
table {
font-size: 1em;
}
.dial-in-conference-id {
margin: 10px 0 10px 0;
}
.dial-in-conference-description {
font-size: 0.8em;
}
.toll-free-list {
min-width: 80px;
}
.numbers-list {
min-width: 150px;
}
}
&__href {

View File

@ -124,29 +124,75 @@
}
}
.dial-in-numbers-list {
margin-top: 20px;
font-size: 12px;
line-height: 24px;
border-collapse: separate;
border-spacing: 0 5px;
thead {
text-align: left;
}
td,
th {
border-bottom: 1px solid #d1dbe8;
}
.flag {
border-bottom-style: none;
width: 30px;
vertical-align: top;
}
.country {
font-weight: bold;
vertical-align: top;
padding: 0 20px 0 0;
}
ul {
padding: 0px 0px 0px 0px;
}
.numbers-list {
list-style: none;
padding: 0 20px 0 0;
}
.toll-free-list {
font-weight: bold;
list-style: none;
vertical-align: top;
}
}
.dial-in-page {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
font-size: 24px;
font-size: 12px;
max-height: 100%;
overflow: auto;
padding: 25px;
position: absolute;
transform: translateY(-50%);
top: 50%;
width: 100%;
.dial-in-numbers-list {
font-size: 24px;
margin-top: 20px;
}
.dial-in-conference-id {
text-align: center;
min-width: 200px;
width: 30%;
}
.dial-in-conference-name,
.dial-in-conference-pin {
font-size: 18px;
}
.dial-in-conference-description {
margin: 12px;
}
}

12
debian/rules vendored
View File

@ -3,12 +3,22 @@
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
LANGUAGES := $(shell node -p "Object.keys(require('./lang/languages.json')).join(' ')")
COUNTRIES_DIR := node_modules/i18n-iso-countries/langs
%:
dh $@
# we skip making Makefile exists for updating browserify modules when developing
override_dh_auto_build:
override_dh_install:
override_dh_install: $(LANGUAGES)
dh_installdirs
dh_install -X/config.js -X/package.json
$(LANGUAGES):
if [ -f $(COUNTRIES_DIR)/$@.json ] ; \
then \
dh_install -pjitsi-meet-web $(COUNTRIES_DIR)/$@.json usr/share/jitsi-meet/lang/; \
mv debian/jitsi-meet-web/usr/share/jitsi-meet/lang/$@.json debian/jitsi-meet-web/usr/share/jitsi-meet/lang/countries-$@.json; \
fi;

View File

@ -71,7 +71,7 @@ paths:
$ref: "#/definitions/ConferenceMapperDetails"
405:
description: "Invalid input"
/phoneNumberList:
get:
tags:
@ -96,7 +96,7 @@ securityDefinitions:
name: "Authorization"
in: "header"
definitions:
ConferenceMapperRequest:
description: "Request to create or find a conference mapping"
type: "object"
@ -114,7 +114,7 @@ definitions:
domain:
type: "string"
description: "Domain part of the conference. Used if 'conference' is not provided. Defaults to domain of the API endpoint. Used to generate a 'conference' value (search by conference)"
ConferenceMapperDetails:
description: "Conference mapping between conference JID and numeric ID"
type: "object"
@ -126,22 +126,26 @@ definitions:
type: "string"
format: "JID"
description: "Full JID for the conference OR boolean false if no conference was found (search by ID)"
PhoneNumberList:
type: "object"
properties:
numbersEnabled:
type: "boolean"
description: "Control flag for Jitsi Meet user interface. Must be set to true for Jitsi Meet to display phone-in UI elements"
numbers:
description: "List of dial in numbers for the conference."
type: "array"
items:
type: "object"
description: "Keys are Country Names, each value is an array of phone numbers"
additionalProperties:
type: "array"
items:
properties:
countryCode:
type: "string"
format: "phone"
description: "ISO 3166-1 country code. Alpha-2 supported."
default:
type: "boolean"
description: "Whether this number is the default one to show. Optional."
formattedNumber:
type: "string"
description: "The formatted telephone number to show."
tollFree:
type: "boolean"
description: "Whether the number is toll free number."
externalDocs:
description: "Find out more about the Jitsi Cloud API"
url: "https://jitsi.org/CloudAPI"
url: "https://jitsi.org/CloudAPI"

View File

@ -344,10 +344,11 @@
"cancelPassword": "Cancel password",
"conferenceURL": "Link:",
"country": "Country",
"dialANumber": "To join your meeting, dial one of these numbers and then enter this PIN: __conferenceID__#",
"dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "Sorry, dialing in is currently not suppported.",
"dialInNotSupported": "Sorry, dialing in is currently not supported.",
"dialInNumber": "Dial-in:",
"dialInTollFree": "Toll Free",
"genericError": "Whoops, something went wrong.",
"inviteLiveStream": "To view the live stream of this meeting, click this link: __url__",
"invitePhone": "To join by phone, dial __number__ and enter this PIN: __conferenceID__#",

5
package-lock.json generated
View File

@ -7494,6 +7494,11 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true
},
"i18n-iso-countries": {
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-3.7.8.tgz",
"integrity": "sha512-NkT3lRiw7D4kKtSAVjVdHCvGlc2UOe0ALKa9IfEx0LkEDf0q3YgjP/veVk0d/OZ7yqUNzV8aJP4lJc6RPj++Gw=="
},
"i18next": {
"version": "8.4.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-8.4.3.tgz",

View File

@ -37,6 +37,7 @@
"@webcomponents/url": "0.7.1",
"amplitude-js": "4.5.2",
"dropbox": "4.0.9",
"i18n-iso-countries": "3.7.8",
"i18next": "8.4.3",
"i18next-browser-languagedetector": "2.0.0",
"i18next-xhr-backend": "1.4.2",

View File

@ -13,7 +13,7 @@ import { translate as reactI18nextTranslate } from 'react-i18next';
export function translate(component, options = { wait: true }) {
// Use the default list of namespaces.
return (
reactI18nextTranslate([ 'main', 'languages' ], options)(
reactI18nextTranslate([ 'main', 'languages', 'countries' ], options)(
component));
}

View File

@ -3,6 +3,7 @@
import i18next from 'i18next';
import I18nextXHRBackend from 'i18next-xhr-backend';
import COUNTRIES_RESOURCES from 'i18n-iso-countries/langs/en.json';
import LANGUAGES_RESOURCES from '../../../../lang/languages.json';
import MAIN_RESOURCES from '../../../../lang/main.json';
@ -51,7 +52,7 @@ const options = {
load: 'unspecific',
ns: {
defaultNs: 'main',
namespaces: [ 'main', 'languages' ]
namespaces: [ 'main', 'languages', 'countries' ]
},
resGetPath: 'lang/__ns__-__lng__.json',
useDataAttrOptions: true
@ -68,6 +69,12 @@ i18next
.init(options);
// Add default language which is preloaded from the source code.
i18next.addResourceBundle(
DEFAULT_LANGUAGE,
'countries',
COUNTRIES_RESOURCES,
/* deep */ true,
/* overwrite */ true);
i18next.addResourceBundle(
DEFAULT_LANGUAGE,
'languages',

View File

@ -14,6 +14,11 @@ type Props = {
*/
conferenceID: number,
/**
* The name of the conference.
*/
conferenceName: ?string,
/**
* Invoked to obtain translated strings.
*/
@ -33,11 +38,19 @@ class ConferenceID extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { conferenceID, t } = this.props;
const { conferenceID, conferenceName, t } = this.props;
return (
<div className = 'dial-in-conference-id'>
{ t('info.dialANumber', { conferenceID }) }
<div className = 'dial-in-conference-name'>
{ conferenceName }
</div>
<div className = 'dial-in-conference-description'>
{ t('info.dialANumber') }
</div>
<div className = 'dial-in-conference-pin'>
{ `${t('info.dialInConferenceID')} ${conferenceID}` }
</div>
</div>
);
}

View File

@ -56,9 +56,9 @@ type State = {
loading: boolean,
/**
* The dial-in numbers. entered by the local participant.
* The dial-in numbers to be displayed.
*/
numbers: ?Array<Object>,
numbers: ?Array<Object> | ?Object,
/**
* Whether or not dial-in is allowed.
@ -143,6 +143,7 @@ class DialInSummary extends Component<Props, State> {
conferenceID
? <ConferenceID
conferenceID = { conferenceID }
conferenceName = { this.props.room }
key = 'conferenceID' />
: null,
<NumbersList
@ -238,17 +239,22 @@ class DialInSummary extends Component<Props, State> {
* Callback invoked when fetching dial-in numbers succeeds. Sets the
* internal to show the numbers.
*
* @param {Object} response - The response from fetching dial-in numbers.
* @param {Array|Object} response - The response from fetching
* dial-in numbers.
* @param {Array|Object} response.numbers - The dial-in numbers.
* @param {boolean} reponse.numbersEnabled - Whether or not dial-in is
* enabled.
* @param {boolean} response.numbersEnabled - Whether or not dial-in is
* enabled, old syntax that is deprecated.
* @private
* @returns {void}
*/
_onGetNumbersSuccess({ numbers, numbersEnabled }) {
_onGetNumbersSuccess(
response: Array<Object> | { numbersEnabled?: boolean }) {
this.setState({
numbersEnabled,
numbers
numbersEnabled:
Array.isArray(response)
? response.length > 0 : response.numbersEnabled,
numbers: response
});
}

View File

@ -17,10 +17,10 @@ type Props = {
conferenceID: number,
/**
* The phone numbers to display. Can be an array of numbers or an object
* with countries as keys and an array of numbers as values.
* The phone numbers to display. Can be an array of number Objects or an
* object with countries as keys and an array of numbers as values.
*/
numbers: { [string]: Array<string> } | Array<string>,
numbers: { [string]: Array<string> } | Array<Object>,
/**
* Invoked to obtain translated strings.
@ -41,92 +41,165 @@ class NumbersList extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { numbers, t } = this.props;
const { numbers } = this.props;
return (
<table className = 'dial-in-numbers-list'>
<thead>
<tr>
{ Array.isArray(numbers)
? null
: <th>{ t('info.country') }</th> }
<th>{ t('info.numbers') }</th>
</tr>
</thead>
<tbody className = 'dial-in-numbers-body'>
{ Array.isArray(numbers)
? numbers.map(this._renderNumberRow)
: this._renderWithCountries(numbers) }
</tbody>
</table>);
return this._renderWithCountries(numbers);
}
/**
* Renders rows of countries and associated phone numbers.
*
* @param {Object} numbersMapping - An object with country names as keys
* and values as arrays of phone numbers.
* @param {Object|Array<Object>} numbersMapping - An object with country
* names as keys and values as arrays of phone numbers.
* @private
* @returns {ReactElement[]}
*/
_renderWithCountries(numbersMapping: Object) {
const rows = [];
_renderWithCountries(
numbersMapping: { numbers: Array<string> } | Array<Object>) {
const { t } = this.props;
let hasFlags = false, numbers;
for (const [ country, numbers ] of Object.entries(numbersMapping)) {
if (!Array.isArray(numbers)) {
return;
}
if (Array.isArray(numbersMapping)) {
hasFlags = true;
numbers = numbersMapping.reduce(
(resultNumbers, number) => {
const countryName
= t(`countries:countries.${number.countryCode}`);
const formattedNumbers = numbers.map(number => {
if (typeof number === 'string') {
return this._renderNumberDiv(number);
if (resultNumbers[countryName]) {
resultNumbers[countryName].push(number);
} else {
resultNumbers[countryName] = [ number ];
}
return resultNumbers;
}, {});
} else {
numbers = {};
for (const [ country, numbersArray ]
of Object.entries(numbersMapping.numbers)) {
if (Array.isArray(numbersArray)) {
/* eslint-disable arrow-body-style */
const formattedNumbers = numbersArray.map(number => ({
formattedNumber: number
}));
/* eslint-enable arrow-body-style */
numbers[country] = formattedNumbers;
}
return null;
});
rows.push(
<tr key = { country }>
<td>{ country }</td>
<td className = 'dial-in-numbers'>{ formattedNumbers }</td>
</tr>
);
}
}
return rows;
}
const rows = [];
Object.keys(numbers).forEach((countryName: string) => {
const numbersArray = numbers[countryName];
rows.push(
<tr
className = 'number-group'
key = { countryName }>
{ this._renderFlag(numbersArray[0].countryCode) }
<td className = 'country' >{ countryName }</td>
<td className = 'numbers-list-column'>
{ this._renderNumbersList(numbersArray) }
</td>
<td className = 'toll-free-list-column' >
{ this._renderNumbersTollFreeList(numbersArray) }
</td>
</tr>
);
});
/**
* Renders a table row for a phone number.
*
* @param {string} number - The phone number to display.
* @private
* @returns {ReactElement[]}
*/
_renderNumberRow(number) {
return (
<tr key = { number }>
<td className = 'dial-in-number'>
{ this._renderNumberLink(number) }
</td>
</tr>
<table className = 'dial-in-numbers-list'>
<thead>
<tr>
{ hasFlags ? <th /> : null}
<th>{ t('info.country') }</th>
<th>{ t('info.numbers') }</th>
<th />
</tr>
</thead>
<tbody className = 'dial-in-numbers-body'>
{ rows }
</tbody>
</table>
);
}
/**
* Renders a div container for a phone number.
*
* @param {string} number - The phone number to display.
* @param {string} countryCode - The phone number to display.
* @private
* @returns {ReactElement}
*/
_renderFlag(countryCode) {
const OFFSET = 127397;
if (countryCode) {
// ensure country code is all caps
const cc = countryCode.toUpperCase();
// return the emoji flag corresponding to country_code or null
const countryFlag = /^[A-Z]{2}$/.test(cc)
? String.fromCodePoint(...[ ...cc ]
.map(c => c.charCodeAt() + OFFSET))
: null;
return <td className = 'flag'>{ countryFlag }</td>;
}
return null;
}
/**
* Renders a div container for a phone number.
*
* @param {Array} numbers - The phone number to display.
* @private
* @returns {ReactElement[]}
*/
_renderNumberDiv(number) {
return (
<div
_renderNumbersList(numbers) {
const numbersListItems = numbers.map(number =>
(<li
className = 'dial-in-number'
key = { number }>
{ this._renderNumberLink(number) }
</div>
key = { number.formattedNumber }>
{ this._renderNumberLink(number.formattedNumber) }
</li>));
return (
<ul className = 'numbers-list'>
{ numbersListItems }
</ul>
);
}
/**
* Renders list with a toll free text on the position where there is a
* number marked as toll free.
*
* @param {Array} numbers - The phone number that are displayed.
* @private
* @returns {ReactElement[]}
*/
_renderNumbersTollFreeList(numbers) {
const { t } = this.props;
const tollNumbersListItems = numbers.map(number =>
(<li
className = 'toll-free'
key = { number.formattedNumber }>
{ number.tollFree ? t('info.dialInTollFree') : '' }
</li>));
return (
<ul className = 'toll-free-list'>
{ tollNumbersListItems }
</ul>
);
}
@ -141,9 +214,12 @@ class NumbersList extends Component<Props> {
*/
_renderNumberLink(number) {
if (this.props.clickableNumbers) {
// Url encode # to %23, Android phone was cutting the # after
// clicking it.
// Seems that using ',' and '%23' works on iOS and Android.
return (
<a
href = { `tel:${number}p${this.props.conferenceID}#` }
href = { `tel:${number},${this.props.conferenceID}%23` }
key = { number } >
{ number }
</a>

View File

@ -121,9 +121,7 @@ class InfoDialog extends Component<Props, State> {
let phoneNumber = state.phoneNumber;
if (!state.phoneNumber && props.dialIn.numbers) {
const { defaultCountry, numbers } = props.dialIn;
phoneNumber = _getDefaultPhoneNumber(numbers, defaultCountry);
phoneNumber = _getDefaultPhoneNumber(props.dialIn);
}
return {
@ -157,11 +155,9 @@ class InfoDialog extends Component<Props, State> {
constructor(props: Props) {
super(props);
const { defaultCountry, numbers } = props.dialIn;
if (numbers) {
if (props.dialIn && props.dialIn.numbers) {
this.state.phoneNumber
= _getDefaultPhoneNumber(numbers, defaultCountry);
= _getDefaultPhoneNumber(props.dialIn.numbers);
}
/**

View File

@ -54,13 +54,14 @@ export function getDialInConferenceID(
/**
* Sends a GET request for phone numbers used to dial into a conference.
*
* @param {string} url - The service that returns confernce dial-in numbers.
* @param {string} url - The service that returns conference dial-in numbers.
* @param {string} roomName - The conference name to find the associated
* conference ID.
* @param {string} mucURL - In which MUC the conference exists.
* @returns {Promise} - The promise created by the request. The returned numbers
* may be an array of numbers or an object with countries as keys and arrays of
* phone number strings.
* may be an array of Objects containing numbers, with keys countryCode,
* tollFree, formattedNumber or an object with countries as keys and arrays of
* phone number strings, as the second one should not be used and is deprecated.
*/
export function getDialInNumbers(
url: string,
@ -432,7 +433,7 @@ export function getShareInfoText(
numbersPromise = Promise.all([
getDialInNumbers(dialInNumbersUrl, room, mucURL),
getDialInConferenceID(dialInConfCodeUrl, room, mucURL)
]).then(([ { defaultCountry, numbers }, {
]).then(([ numbers, {
conference, id, message } ]) => {
if (!conference || !id) {
@ -440,7 +441,6 @@ export function getShareInfoText(
}
return {
defaultCountry,
numbers,
conferenceID: id
};
@ -448,9 +448,8 @@ export function getShareInfoText(
}
return numbersPromise.then(
({ conferenceID, defaultCountry, numbers }) => {
const phoneNumber
= _getDefaultPhoneNumber(numbers, defaultCountry) || '';
({ conferenceID, numbers }) => {
const phoneNumber = _getDefaultPhoneNumber(numbers) || '';
return `${
i18next.t('info.dialInNumber')} ${
@ -513,27 +512,47 @@ export function getDialInfoPageURL(
*
* @param {Array<string>|Object} dialInNumbers - The array or object of
* numbers to choose a number from.
* @param {string} defaultCountry - The country code for the country
* whose phone number should display.
* @private
* @returns {string|null}
*/
export function _getDefaultPhoneNumber(
dialInNumbers: Object,
defaultCountry: string = 'US'): ?string {
dialInNumbers: Object): ?string {
const defValueForDefaultCountry = 'US';
if (Array.isArray(dialInNumbers)) {
// Dumbly return the first number if an array.
return dialInNumbers[0];
} else if (Object.keys(dialInNumbers).length > 0) {
const defaultNumbers = dialInNumbers[defaultCountry];
// new syntax follows
// find the default country inside dialInNumbers, US one
// or return the first one
let defaultNumber = dialInNumbers.find(number => number.default);
if (!defaultNumber) {
defaultNumber = dialInNumbers.find(({ countryCode }) =>
countryCode === defValueForDefaultCountry);
}
if (defaultNumber) {
return defaultNumber.formattedNumber;
}
return dialInNumbers.length > 0
? dialInNumbers[0].formattedNumber : null;
}
const {
defaultCountry = defValueForDefaultCountry,
numbers } = dialInNumbers;
if (numbers && Object.keys(numbers).length > 0) {
// deprecated and will be removed
const defaultNumbers = numbers[defaultCountry];
if (defaultNumbers) {
return defaultNumbers[0];
}
const firstRegion = Object.keys(dialInNumbers)[0];
const firstRegion = Object.keys(numbers)[0];
return firstRegion && firstRegion[0];
return firstRegion && numbers[firstRegion][0];
}
return null;

View File

@ -10,6 +10,8 @@ import {
UPDATE_DIAL_IN_NUMBERS_SUCCESS
} from './actionTypes';
const logger = require('jitsi-meet-logger').getLogger(__filename);
const DEFAULT_STATE = {
/**
* The indicator which determines whether (the) {@code CalleeInfo} is
@ -54,17 +56,24 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
};
case UPDATE_DIAL_IN_NUMBERS_SUCCESS: {
const {
defaultCountry,
numbers,
numbersEnabled
} = action.dialInNumbers;
if (Array.isArray(action.dialInNumbers)) {
return {
...state,
conferenceID: action.conferenceID,
numbers: action.dialInNumbers,
numbersEnabled: true
};
}
// this is the old format which is deprecated
logger.warn('Using deprecated API for retrieving phone numbers');
const { numbersEnabled } = action.dialInNumbers;
return {
...state,
conferenceID: action.conferenceID,
defaultCountry,
numbers,
numbers: action.dialInNumbers,
numbersEnabled
};
}