jiti-meet/react/features/invite/functions.js

553 lines
17 KiB
JavaScript
Raw Normal View History

// @flow
import { getAppProp } from '../base/app';
import { i18next } from '../base/i18n';
import { isLocalParticipantModerator } from '../base/participants';
import { doGetJSON, parseURIString } from '../base/util';
2017-06-14 18:41:22 +00:00
declare var $: Function;
WiP(invite-ui): Initial move of invite UI to invite button (#1950) * WiP(invite-ui): Initial move of invite UI to invite button * Adjusts styling to fit both horizontal and vertical filmstrip * Removes comment and functions not needed * [squash] Addressing various review comments * [squash] Move invite options to a separate config * [squash] Adjust invite button styles until we fix the whole UI theme * [squash] Fix the remote videos scroll * [squash]:Do not show popup menu when 1 option is available * [squash]: Disable the invite button in filmstrip mode * feat(connection-indicator): implement automatic hiding on good connection (#2009) * ref(connection-stats): use PropTypes package * feat(connection-stats): display a summary of the connection quality * feat(connection-indicator): show empty bars for interrupted connection * feat(connection-indicator): change background color based on status * feat(connection-indicator): implement automatic hiding on good connection * fix(connection-indicator): explicitly set font size Currently non-react code will set an icon size on ConnectionIndicator. This doesn't work on initial call join in vertical filmstrip after some changes to support hiding the indicator. The chosen fix is passing in the icon size to mirror what would happe with full filmstrip reactification. * ref(connection-stats): rename statuses * feat(connection-indicator): make hiding behavior configurable The original implementation made the auto hiding of the indicator configured in interfaceConfig. * fix(connection-indicator): readd class expected by torture tests * fix(connection-indicator): change connection quality display styling Bold the connection summary in the stats popover so it stands out. Change the summaries so there are only three--strong, nonoptimal, poor. * fix(connection-indicator): gray background on lost connection * feat(icons): add new gsm bars icon * feat(connection-indicator): use new 3-bar icon * ref(icons): remove icon-connection and icon-connection-lost Both have been replaced by icon-gsm-bars so they are not being referenced anymore. Mobile looks to have connect-lost as a separate icon in font-icons/jitsi.json. * fix(defaultToolbarButtons): Fixes unresolved InfoDialogButton component problem * [squash]: Makes invite button fit the container * [squash]:Addressing invite truncate, remote menu position and comment * [squash]:Fix z-index in horizontal mode, z-index in lonely call * [squash]: Fix filmstripOnly property, remove important from css
2017-10-03 16:30:42 +00:00
declare var interfaceConfig: Object;
2017-06-14 18:41:22 +00:00
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Sends an ajax request to check if the phone number can be called.
*
* @param {string} dialNumber - The dial number to check for validity.
* @param {string} dialOutAuthUrl - The endpoint to use for checking validity.
* @returns {Promise} - The promise created by the request.
*/
export function checkDialNumber(
dialNumber: string,
dialOutAuthUrl: string
): Promise<Object> {
const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`;
return new Promise((resolve, reject) => {
$.getJSON(fullUrl)
.then(resolve)
.catch(reject);
});
}
/**
* Sends a GET request to obtain the conference ID necessary for identifying
* which conference to join after diaing the dial-in service.
*
* @param {string} baseUrl - The url for obtaining the conference ID (pin) for
* dialing into a conference.
* @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.
*/
export function getDialInConferenceID(
baseUrl: string,
roomName: string,
mucURL: string
): Promise<Object> {
const conferenceIDURL = `${baseUrl}?conference=${roomName}@${mucURL}`;
return doGetJSON(conferenceIDURL);
}
/**
* Sends a GET request for phone numbers used to dial into a conference.
*
* @param {string} url - The service that returns confernce dial-in numbers.
* @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.
*/
export function getDialInNumbers(url: string): Promise<*> {
return doGetJSON(url);
}
/**
* Removes all non-numeric characters from a string.
*
* @param {string} text - The string from which to remove all characters except
* numbers.
* @returns {string} A string with only numbers.
*/
export function getDigitsOnly(text: string = ''): string {
return text.replace(/\D/g, '');
}
/**
* Type of the options to use when sending a search query.
*/
export type GetInviteResultsOptions = {
/**
* The endpoint to use for checking phone number validity.
*/
dialOutAuthUrl: string,
/**
* Whether or not to search for people.
*/
addPeopleEnabled: boolean,
/**
* Whether or not to check phone numbers.
*/
dialOutEnabled: boolean,
/**
* Array with the query types that will be executed -
* "conferenceRooms" | "user" | "room".
*/
peopleSearchQueryTypes: Array<string>,
/**
* The url to query for people.
*/
peopleSearchUrl: string,
/**
* The jwt token to pass to the search service.
*/
jwt: string
};
/**
* Combines directory search with phone number validation to produce a single
* set of invite search results.
*
* @param {string} query - Text to search.
* @param {GetInviteResultsOptions} options - Options to use when searching.
* @returns {Promise<*>}
*/
export function getInviteResultsForQuery(
query: string,
options: GetInviteResultsOptions
): Promise<*> {
const text = query.trim();
const {
dialOutAuthUrl,
addPeopleEnabled,
dialOutEnabled,
peopleSearchQueryTypes,
peopleSearchUrl,
jwt
} = options;
let peopleSearchPromise;
if (addPeopleEnabled && text) {
peopleSearchPromise = searchDirectory(
peopleSearchUrl,
jwt,
text,
peopleSearchQueryTypes);
} else {
peopleSearchPromise = Promise.resolve([]);
}
let hasCountryCode = text.startsWith('+');
let phoneNumberPromise;
// Phone numbers are handled a specially to enable both cases of restricting
// numbers to telephone number-y numbers and accepting any arbitrary string,
// which may be valid for SIP (jigasi) calls. If the dialOutAuthUrl is
// defined, then it is assumed the call is to a telephone number and
// some validation of the number is completed, with the + sign used as a way
// for the UI to detect and enforce the usage of a country code. If the
// dialOutAuthUrl is not defined, accept anything because this is assumed
// to be the SIP (jigasi) case.
if (dialOutEnabled && dialOutAuthUrl && isMaybeAPhoneNumber(text)) {
let numberToVerify = text;
// When the number to verify does not start with a +, we assume no
// proper country code has been entered. In such a case, prepend 1 for
// the country code. The service currently takes care of prepending the
// +.
if (!hasCountryCode && !text.startsWith('1')) {
numberToVerify = `1${numberToVerify}`;
}
// The validation service works properly when the query is digits only
// so ensure only digits get sent.
numberToVerify = getDigitsOnly(numberToVerify);
phoneNumberPromise = checkDialNumber(numberToVerify, dialOutAuthUrl);
} else if (dialOutEnabled && !dialOutAuthUrl) {
// fake having a country code to hide the country code reminder
hasCountryCode = true;
// With no auth url, let's say the text is a valid number
phoneNumberPromise = Promise.resolve({
allow: true,
country: '',
phone: text
});
} else {
phoneNumberPromise = Promise.resolve({});
}
return Promise.all([ peopleSearchPromise, phoneNumberPromise ])
.then(([ peopleResults, phoneResults ]) => {
const results = [
...peopleResults
];
/**
* This check for phone results is for the day the call to searching
* people might return phone results as well. When that day comes
* this check will make it so the server checks are honored and the
* local appending of the number is not done. The local appending of
* the phone number can then be cleaned up when convenient.
*/
const hasPhoneResult
= peopleResults.find(result => result.type === 'phone');
if (!hasPhoneResult && typeof phoneResults.allow === 'boolean') {
results.push({
allowed: phoneResults.allow,
country: phoneResults.country,
type: 'phone',
number: phoneResults.phone,
originalEntry: text,
showCountryCodeReminder: !hasCountryCode
});
}
return results;
});
}
/**
* Helper for determining how many of each type of user is being invited. Used
* for logging and sending analytics related to invites.
*
* @param {Array} inviteItems - An array with the invite items, as created in
* {@link _parseQueryResults}.
* @returns {Object} An object with keys as user types and values as the number
* of invites for that type.
*/
export function getInviteTypeCounts(inviteItems: Array<Object> = []) {
const inviteTypeCounts = {};
inviteItems.forEach(({ type }) => {
if (!inviteTypeCounts[type]) {
inviteTypeCounts[type] = 0;
}
inviteTypeCounts[type]++;
});
return inviteTypeCounts;
}
/**
* Sends a post request to an invite service.
*
* @param {string} inviteServiceUrl - The invite service that generates the
* invitation.
* @param {string} inviteUrl - The url to the conference.
* @param {string} jwt - The jwt token to pass to the search service.
* @param {Immutable.List} inviteItems - The list of the "user" or "room" type
* items to invite.
* @returns {Promise} - The promise created by the request.
*/
export function invitePeopleAndChatRooms( // eslint-disable-line max-params
inviteServiceUrl: string,
inviteUrl: string,
jwt: string,
inviteItems: Array<Object>
): Promise<void> {
if (!inviteItems || inviteItems.length === 0) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
$.post(
`${inviteServiceUrl}?token=${jwt}`,
JSON.stringify({
'invited': inviteItems,
'url': inviteUrl
}),
resolve,
'json')
.fail((jqxhr, textStatus, error) => reject(error));
});
}
/**
* Determines if adding people is currently enabled.
*
* @param {boolean} state - Current state.
* @returns {boolean} Indication of whether adding people is currently enabled.
*/
export function isAddPeopleEnabled(state: Object): boolean {
const { isGuest } = state['features/base/jwt'];
if (!isGuest) {
// XXX The mobile/react-native app is capable of disabling the
// adding/inviting of people in the current conference. Anyway, the
// Web/React app does not have that capability so default appropriately.
const addPeopleEnabled = getAppProp(state, 'addPeopleEnabled');
return (
(typeof addPeopleEnabled === 'undefined')
|| Boolean(addPeopleEnabled));
}
return false;
}
/**
* Determines if dial out is currently enabled or not.
*
* @param {boolean} state - Current state.
* @returns {boolean} Indication of whether dial out is currently enabled.
*/
export function isDialOutEnabled(state: Object): boolean {
const { conference } = state['features/base/conference'];
let dialOutEnabled = isLocalParticipantModerator(state)
&& conference
&& conference.isSIPCallingSupported();
if (dialOutEnabled) {
// XXX The mobile/react-native app is capable of disabling of dial-out.
// Anyway, the Web/React app does not have that capability so default
// appropriately.
dialOutEnabled = getAppProp(state, 'dialOutEnabled');
return (
(typeof dialOutEnabled === 'undefined') || Boolean(dialOutEnabled));
}
return false;
}
/**
* Checks whether a string looks like it could be for a phone number.
*
* @param {string} text - The text to check whether or not it could be a phone
* number.
* @private
* @returns {boolean} True if the string looks like it could be a phone number.
*/
function isMaybeAPhoneNumber(text: string): boolean {
if (!isPhoneNumberRegex().test(text)) {
return false;
}
const digits = getDigitsOnly(text);
return Boolean(digits.length);
}
/**
* RegExp to use to determine if some text might be a phone number.
*
* @returns {RegExp}
*/
function isPhoneNumberRegex(): RegExp {
let regexString = '^[0-9+()-\\s]*$';
if (typeof interfaceConfig !== 'undefined') {
regexString = interfaceConfig.PHONE_NUMBER_REGEX || regexString;
}
return new RegExp(regexString);
}
/**
* Sends an ajax request to a directory service.
*
* @param {string} serviceUrl - The service to query.
* @param {string} jwt - The jwt token to pass to the search service.
* @param {string} text - Text to search.
* @param {Array<string>} queryTypes - Array with the query types that will be
* executed - "conferenceRooms" | "user" | "room".
* @returns {Promise} - The promise created by the request.
*/
export function searchDirectory( // eslint-disable-line max-params
serviceUrl: string,
jwt: string,
text: string,
queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
): Promise<Array<Object>> {
const query = encodeURIComponent(text);
const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
return fetch(`${serviceUrl}?query=${query}&queryTypes=${
queryTypesString}&jwt=${jwt}`)
.then(response => {
const jsonify = response.json();
if (response.ok) {
return jsonify;
}
return jsonify
.then(result => Promise.reject(result));
})
.catch(error => {
logger.error(
'Error searching directory:', error);
return Promise.reject(error);
});
}
/**
* Returns descriptive text that can be used to invite participants to a meeting
* (share via mobile or use it for calendar event description).
*
* @param {Object} state - The current state.
* @param {string} inviteUrl - The conference/location URL.
* @param {boolean} useHtml - Whether to return html text.
* @returns {Promise<string>} A {@code Promise} resolving with a
* descriptive text that can be used to invite participants to a meeting.
*/
export function getShareInfoText(
state: Object, inviteUrl: string, useHtml: ?boolean): Promise<string> {
let roomUrl = inviteUrl;
const includeDialInfo = state['features/base/config'] !== undefined;
if (useHtml) {
roomUrl = `<a href="${roomUrl}">${roomUrl}</a>`;
}
let infoText = i18next.t('share.mainText', { roomUrl });
if (includeDialInfo) {
const { room } = parseURIString(inviteUrl);
let numbersPromise;
if (state['features/invite'].numbers
&& state['features/invite'].conferenceID) {
numbersPromise = Promise.resolve(state['features/invite']);
} else {
// we are requesting numbers and conferenceId directly
// not using updateDialInNumbers, because custom room
// is specified and we do not want to store the data
// in the state
const { dialInConfCodeUrl, dialInNumbersUrl, hosts }
= state['features/base/config'];
const mucURL = hosts && hosts.muc;
if (!dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
// URLs for fetching dial in numbers not defined
2018-09-04 07:29:48 +00:00
return Promise.resolve(infoText);
}
numbersPromise = Promise.all([
getDialInNumbers(dialInNumbersUrl),
getDialInConferenceID(dialInConfCodeUrl, room, mucURL)
]).then(([ { defaultCountry, numbers }, {
conference, id, message } ]) => {
if (!conference || !id) {
return Promise.reject(message);
}
return {
defaultCountry,
numbers,
conferenceID: id
};
});
}
return numbersPromise.then(
({ conferenceID, defaultCountry, numbers }) => {
const phoneNumber
= _getDefaultPhoneNumber(numbers, defaultCountry) || '';
return `${
i18next.t('info.dialInNumber')} ${
phoneNumber} ${
i18next.t('info.dialInConferenceID')} ${
conferenceID}#\n\n`;
})
.catch(error =>
logger.error('Error fetching numbers or conferenceID', error))
.then(defaultDialInNumber => {
let dialInfoPageUrl = getDialInfoPageURL(
room,
state['features/base/connection'].locationURL);
if (useHtml) {
dialInfoPageUrl
= `<a href="${dialInfoPageUrl}">${dialInfoPageUrl}</a>`;
}
infoText += i18next.t('share.dialInfoText', {
defaultDialInNumber,
dialInfoPageUrl });
return infoText;
});
}
return Promise.resolve(infoText);
}
/**
* Generates the URL for the static dial in info page.
*
* @param {string} conferenceName - The conference name.
* @param {Object} locationURL - The current location URL, the object coming
* from state ['features/base/connection'].locationURL.
* @returns {string}
*/
export function getDialInfoPageURL(
conferenceName: string,
locationURL: Object) {
const origin = locationURL.origin;
const pathParts = locationURL.pathname.split('/');
pathParts.length = pathParts.length - 1;
const newPath = pathParts.reduce((accumulator, currentValue) => {
if (currentValue) {
return `${accumulator}/${currentValue}`;
}
return accumulator;
}, '');
return `${origin}${newPath}/static/dialInInfo.html?room=${conferenceName}`;
}
/**
* Sets the internal state of which dial-in number to display.
*
* @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 {
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];
if (defaultNumbers) {
return defaultNumbers[0];
}
const firstRegion = Object.keys(dialInNumbers)[0];
return firstRegion && firstRegion[0];
}
return null;
}