mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-17 18:57:46 +00:00
[RN] add support for inviting participants during a call on mobile
* Button conditionally shown based on if the feature is enabled and available * Hooks for launching the invite UI (delegates to the native layer) * Hooks for using the search and dial out checks from the native layer (calls back into JS) * Hooks for handling sending invites and passing any failures back to the native layer * Android and iOS handling for those hooks Author: Ryan Peck <rpeck@atlassian.com> Author: Eric Brynsvold <ebrynsvold@atlassian.com>
This commit is contained in:
committed by
Saúl Ibarra Corretgé
parent
4e36127dc7
commit
f64c13d4b7
@@ -37,6 +37,10 @@ export class App extends AbstractApp {
|
||||
static propTypes = {
|
||||
...AbstractApp.propTypes,
|
||||
|
||||
addPeopleEnabled: PropTypes.bool,
|
||||
|
||||
dialOutEnabled: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Whether Picture-in-Picture is enabled. If {@code true}, a toolbar
|
||||
* button is rendered in the {@link Conference} view to afford entering
|
||||
|
||||
@@ -14,18 +14,14 @@ import { MultiSelectAutocomplete } from '../../base/react';
|
||||
import { inviteVideoRooms } from '../../videosipgw';
|
||||
|
||||
import {
|
||||
checkDialNumber,
|
||||
invitePeopleAndChatRooms,
|
||||
searchDirectory
|
||||
sendInvitesForItems,
|
||||
getInviteResultsForQuery
|
||||
} from '../functions';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
const isPhoneNumberRegex
|
||||
= new RegExp(interfaceConfig.PHONE_NUMBER_REGEX || '^[0-9+()-\\s]*$');
|
||||
|
||||
/**
|
||||
* The dialog that allows to invite people to the call.
|
||||
*/
|
||||
@@ -240,20 +236,6 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
);
|
||||
}
|
||||
|
||||
_getDigitsOnly: (string) => string;
|
||||
|
||||
/**
|
||||
* Removes all non-numeric characters from a string.
|
||||
*
|
||||
* @param {string} text - The string from which to remove all characters
|
||||
* except numbers.
|
||||
* @private
|
||||
* @returns {string} A string with only numbers.
|
||||
*/
|
||||
_getDigitsOnly(text = '') {
|
||||
return text.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for determining how many of each type of user is being invited.
|
||||
* Used for logging and sending analytics related to invites.
|
||||
@@ -294,27 +276,6 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
|| this.state.addToCallInProgress;
|
||||
}
|
||||
|
||||
_isMaybeAPhoneNumber: (string) => boolean;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
_isMaybeAPhoneNumber(text) {
|
||||
if (!isPhoneNumberRegex.test(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const digits = this._getDigitsOnly(text);
|
||||
|
||||
return Boolean(digits.length);
|
||||
}
|
||||
|
||||
_onItemSelected: (Object) => Object;
|
||||
|
||||
/**
|
||||
@@ -379,75 +340,26 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
addToCallInProgress: true
|
||||
});
|
||||
|
||||
let allInvitePromises = [];
|
||||
let invitesLeftToSend = [
|
||||
...this.state.inviteItems
|
||||
];
|
||||
const {
|
||||
_conference,
|
||||
_inviteServiceUrl,
|
||||
_inviteUrl,
|
||||
_jwt
|
||||
} = this.props;
|
||||
|
||||
// First create all promises for dialing out.
|
||||
if (this.props.enableDialOut && this.props._conference) {
|
||||
const phoneNumbers = invitesLeftToSend.filter(
|
||||
({ item }) => item.type === 'phone');
|
||||
const inviteItems = this.state.inviteItems;
|
||||
const items = inviteItems.map(item => item.item);
|
||||
|
||||
// For each number, dial out. On success, remove the number from
|
||||
// {@link invitesLeftToSend}.
|
||||
const phoneInvitePromises = phoneNumbers.map(number => {
|
||||
const numberToInvite = this._getDigitsOnly(number.item.number);
|
||||
const options = {
|
||||
conference: _conference,
|
||||
inviteServiceUrl: _inviteServiceUrl,
|
||||
inviteUrl: _inviteUrl,
|
||||
inviteVideoRooms: this.props.inviteVideoRooms,
|
||||
jwt: _jwt
|
||||
};
|
||||
|
||||
return this.props._conference.dial(numberToInvite)
|
||||
.then(() => {
|
||||
invitesLeftToSend
|
||||
= invitesLeftToSend.filter(invite =>
|
||||
invite !== number);
|
||||
})
|
||||
.catch(error => logger.error(
|
||||
'Error inviting phone number:', error));
|
||||
|
||||
});
|
||||
|
||||
allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
|
||||
}
|
||||
|
||||
if (this.props.enableAddPeople) {
|
||||
const usersAndRooms = invitesLeftToSend.filter(i =>
|
||||
i.item.type === 'user' || i.item.type === 'room')
|
||||
.map(i => i.item);
|
||||
|
||||
if (usersAndRooms.length) {
|
||||
// Send a request to invite all the rooms and users. On success,
|
||||
// filter all rooms and users from {@link invitesLeftToSend}.
|
||||
const peopleInvitePromise = invitePeopleAndChatRooms(
|
||||
this.props._inviteServiceUrl,
|
||||
this.props._inviteUrl,
|
||||
this.props._jwt,
|
||||
usersAndRooms)
|
||||
.then(() => {
|
||||
invitesLeftToSend = invitesLeftToSend.filter(i =>
|
||||
i.item.type !== 'user' && i.item.type !== 'room');
|
||||
})
|
||||
.catch(error => logger.error(
|
||||
'Error inviting people:', error));
|
||||
|
||||
allInvitePromises.push(peopleInvitePromise);
|
||||
}
|
||||
|
||||
// Sipgw calls are fire and forget. Invite them to the conference
|
||||
// then immediately remove them from {@link invitesLeftToSend}.
|
||||
const vrooms = invitesLeftToSend.filter(i =>
|
||||
i.item.type === 'videosipgw')
|
||||
.map(i => i.item);
|
||||
|
||||
this.props._conference
|
||||
&& vrooms.length > 0
|
||||
&& this.props.inviteVideoRooms(
|
||||
this.props._conference, vrooms);
|
||||
|
||||
invitesLeftToSend = invitesLeftToSend.filter(i =>
|
||||
i.item.type !== 'videosipgw');
|
||||
}
|
||||
|
||||
Promise.all(allInvitePromises)
|
||||
.then(() => {
|
||||
sendInvitesForItems(items, options)
|
||||
.then(invitesLeftToSend => {
|
||||
// If any invites are left that means something failed to send
|
||||
// so treat it as an error.
|
||||
if (invitesLeftToSend.length) {
|
||||
@@ -467,8 +379,18 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
addToCallError: true
|
||||
});
|
||||
|
||||
const unsentInviteIDs = invitesLeftToSend.map(invite =>
|
||||
invite.id || invite.number
|
||||
);
|
||||
|
||||
const itemsToSelect = inviteItems.filter(invite =>
|
||||
unsentInviteIDs.includes(
|
||||
invite.item.id || invite.item.number
|
||||
)
|
||||
);
|
||||
|
||||
if (this._multiselect) {
|
||||
this._multiselect.setSelectedItems(invitesLeftToSend);
|
||||
this._multiselect.setSelectedItems(itemsToSelect);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -558,82 +480,25 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_query(query = '') {
|
||||
const text = query.trim();
|
||||
const {
|
||||
enableAddPeople,
|
||||
enableDialOut,
|
||||
_dialOutAuthUrl,
|
||||
_jwt,
|
||||
_peopleSearchQueryTypes,
|
||||
_peopleSearchUrl
|
||||
} = this.props;
|
||||
|
||||
let peopleSearchPromise;
|
||||
const options = {
|
||||
dialOutAuthUrl: _dialOutAuthUrl,
|
||||
enableAddPeople,
|
||||
enableDialOut,
|
||||
jwt: _jwt,
|
||||
peopleSearchQueryTypes: _peopleSearchQueryTypes,
|
||||
peopleSearchUrl: _peopleSearchUrl
|
||||
};
|
||||
|
||||
if (this.props.enableAddPeople && text) {
|
||||
peopleSearchPromise = searchDirectory(
|
||||
_peopleSearchUrl,
|
||||
_jwt,
|
||||
text,
|
||||
_peopleSearchQueryTypes);
|
||||
} else {
|
||||
peopleSearchPromise = Promise.resolve([]);
|
||||
}
|
||||
|
||||
|
||||
const hasCountryCode = text.startsWith('+');
|
||||
let phoneNumberPromise;
|
||||
|
||||
if (this.props.enableDialOut && this._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 = this._getDigitsOnly(numberToVerify);
|
||||
|
||||
phoneNumberPromise
|
||||
= checkDialNumber(numberToVerify, _dialOutAuthUrl);
|
||||
} 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;
|
||||
});
|
||||
return getInviteResultsForQuery(query, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
91
react/features/invite/components/InviteButton.native.js
Normal file
91
react/features/invite/components/InviteButton.native.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { launchNativeInvite } from '../../mobile/invite-search';
|
||||
import { ToolbarButton } from '../../toolbox';
|
||||
|
||||
/**
|
||||
* The type of {@link EnterPictureInPictureToobarButton}'s React
|
||||
* {@code Component} props.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates if the "Add to call" feature is available.
|
||||
*/
|
||||
enableAddPeople: boolean,
|
||||
|
||||
/**
|
||||
* Indicates if the "Dial out" feature is available.
|
||||
*/
|
||||
enableDialOut: boolean,
|
||||
|
||||
/**
|
||||
* Launches native invite dialog.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
onLaunchNativeInvite: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a {@link ToolbarButton} to enter Picture-in-Picture.
|
||||
*/
|
||||
class InviteButton extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
enableAddPeople,
|
||||
enableDialOut,
|
||||
onLaunchNativeInvite,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
if (!enableAddPeople && !enableDialOut) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
iconName = { 'add' }
|
||||
onClick = { onLaunchNativeInvite }
|
||||
{ ...props } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux actions to {@link InviteButton}'s React
|
||||
* {@code Component} props.
|
||||
*
|
||||
* @param {Function} dispatch - The redux action {@code dispatch} function.
|
||||
* @returns {{
|
||||
* onLaunchNativeInvite
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
function _mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
|
||||
/**
|
||||
* Launches native invite dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
* @type {Function}
|
||||
*/
|
||||
onLaunchNativeInvite() {
|
||||
dispatch(launchNativeInvite());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(undefined, _mapDispatchToProps)(InviteButton);
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../base/participants';
|
||||
import { doGetJSON } from '../base/util';
|
||||
|
||||
declare var $: Function;
|
||||
@@ -50,7 +51,7 @@ export function getDialInNumbers(url: string): Promise<*> {
|
||||
* type items to invite.
|
||||
* @returns {Promise} - The promise created by the request.
|
||||
*/
|
||||
export function invitePeopleAndChatRooms( // eslint-disable-line max-params
|
||||
function invitePeopleAndChatRooms( // eslint-disable-line max-params
|
||||
inviteServiceUrl: string,
|
||||
inviteUrl: string,
|
||||
jwt: string,
|
||||
@@ -88,9 +89,10 @@ export function searchDirectory( // eslint-disable-line max-params
|
||||
text: string,
|
||||
queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
|
||||
): Promise<Array<Object>> {
|
||||
const queryTypesString = JSON.stringify(queryTypes);
|
||||
const query = encodeURIComponent(text);
|
||||
const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
|
||||
|
||||
return fetch(`${serviceUrl}?query=${encodeURIComponent(text)}&queryTypes=${
|
||||
return fetch(`${serviceUrl}?query=${query}&queryTypes=${
|
||||
queryTypesString}&jwt=${jwt}`)
|
||||
.then(response => {
|
||||
const jsonify = response.json();
|
||||
@@ -110,6 +112,21 @@ export function searchDirectory( // eslint-disable-line max-params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 check if the phone number can be called.
|
||||
*
|
||||
@@ -137,3 +154,315 @@ export function checkDialNumber(
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all non-numeric characters from a string.
|
||||
*
|
||||
* @param {string} text - The string from which to remove all characters
|
||||
* except numbers.
|
||||
* @private
|
||||
* @returns {string} A string with only numbers.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
enableAddPeople: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not to check phone numbers.
|
||||
*/
|
||||
enableDialOut: 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,
|
||||
enableAddPeople,
|
||||
enableDialOut,
|
||||
peopleSearchQueryTypes,
|
||||
peopleSearchUrl,
|
||||
jwt
|
||||
} = options;
|
||||
|
||||
let peopleSearchPromise;
|
||||
|
||||
if (enableAddPeople && text) {
|
||||
peopleSearchPromise = searchDirectory(
|
||||
peopleSearchUrl,
|
||||
jwt,
|
||||
text,
|
||||
peopleSearchQueryTypes);
|
||||
} else {
|
||||
peopleSearchPromise = Promise.resolve([]);
|
||||
}
|
||||
|
||||
|
||||
const hasCountryCode = text.startsWith('+');
|
||||
let phoneNumberPromise;
|
||||
|
||||
if (enableDialOut && 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 {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of the options to use when sending invites.
|
||||
*/
|
||||
export type SendInvitesOptions = {
|
||||
|
||||
/**
|
||||
* Conference object used to dial out.
|
||||
*/
|
||||
conference: Object,
|
||||
|
||||
/**
|
||||
* The URL to send invites through.
|
||||
*/
|
||||
inviteServiceUrl: string,
|
||||
|
||||
/**
|
||||
* The URL sent with each invite.
|
||||
*/
|
||||
inviteUrl: string,
|
||||
|
||||
/**
|
||||
* The function to use to invite video rooms.
|
||||
*
|
||||
* @param {Object} The conference to which the video rooms should be
|
||||
* invited.
|
||||
* @param {Array<Object>} The list of rooms that should be invited.
|
||||
* @returns {void}
|
||||
*/
|
||||
inviteVideoRooms: (Object, Array<Object>) => void,
|
||||
|
||||
/**
|
||||
* The jwt token to pass to the invite service.
|
||||
*/
|
||||
jwt: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Send invites for a list of items (may be a combination of users, rooms, phone
|
||||
* numbers, and video rooms).
|
||||
*
|
||||
* @param {Array<Object>} invites - Items for which invites should be sent.
|
||||
* @param {SendInvitesOptions} options - Options to use when sending the
|
||||
* provided invites.
|
||||
* @returns {Promise} Promise containing the list of invites that were not sent.
|
||||
*/
|
||||
export function sendInvitesForItems(
|
||||
invites: Array<Object>,
|
||||
options: SendInvitesOptions
|
||||
): Promise<Array<Object>> {
|
||||
|
||||
const {
|
||||
conference,
|
||||
inviteServiceUrl,
|
||||
inviteUrl,
|
||||
inviteVideoRooms,
|
||||
jwt
|
||||
} = options;
|
||||
|
||||
let allInvitePromises = [];
|
||||
let invitesLeftToSend = [ ...invites ];
|
||||
|
||||
// First create all promises for dialing out.
|
||||
if (conference) {
|
||||
const phoneNumbers = invitesLeftToSend.filter(
|
||||
item => item.type === 'phone');
|
||||
|
||||
// For each number, dial out. On success, remove the number from
|
||||
// {@link invitesLeftToSend}.
|
||||
const phoneInvitePromises = phoneNumbers.map(item => {
|
||||
const numberToInvite = getDigitsOnly(item.number);
|
||||
|
||||
return conference.dial(numberToInvite)
|
||||
.then(() => {
|
||||
invitesLeftToSend
|
||||
= invitesLeftToSend.filter(invite =>
|
||||
invite !== item);
|
||||
})
|
||||
.catch(error => logger.error(
|
||||
'Error inviting phone number:', error));
|
||||
|
||||
});
|
||||
|
||||
allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
|
||||
}
|
||||
|
||||
const usersAndRooms = invitesLeftToSend.filter(item =>
|
||||
item.type === 'user' || item.type === 'room');
|
||||
|
||||
if (usersAndRooms.length) {
|
||||
// Send a request to invite all the rooms and users. On success,
|
||||
// filter all rooms and users from {@link invitesLeftToSend}.
|
||||
const peopleInvitePromise = invitePeopleAndChatRooms(
|
||||
inviteServiceUrl,
|
||||
inviteUrl,
|
||||
jwt,
|
||||
usersAndRooms)
|
||||
.then(() => {
|
||||
invitesLeftToSend = invitesLeftToSend.filter(item =>
|
||||
item.type !== 'user' && item.type !== 'room');
|
||||
})
|
||||
.catch(error => logger.error(
|
||||
'Error inviting people:', error));
|
||||
|
||||
allInvitePromises.push(peopleInvitePromise);
|
||||
}
|
||||
|
||||
// Sipgw calls are fire and forget. Invite them to the conference
|
||||
// then immediately remove them from {@link invitesLeftToSend}.
|
||||
const vrooms = invitesLeftToSend.filter(item =>
|
||||
item.type === 'videosipgw');
|
||||
|
||||
conference
|
||||
&& vrooms.length > 0
|
||||
&& inviteVideoRooms(conference, vrooms);
|
||||
|
||||
invitesLeftToSend = invitesLeftToSend.filter(item =>
|
||||
item.type !== 'videosipgw');
|
||||
|
||||
return Promise.all(allInvitePromises)
|
||||
.then(() => invitesLeftToSend);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 { app } = state['features/app'];
|
||||
const { isGuest } = state['features/base/jwt'];
|
||||
|
||||
return !isGuest && Boolean(app && app.props.addPeopleEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'];
|
||||
const { isGuest } = state['features/base/jwt'];
|
||||
const { enableUserRolesBasedOnToken } = state['features/base/config'];
|
||||
const participant = getLocalParticipant(state);
|
||||
|
||||
return participant && participant.role === PARTICIPANT_ROLE.MODERATOR
|
||||
&& conference && conference.isSIPCallingSupported()
|
||||
&& (!enableUserRolesBasedOnToken || !isGuest);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './actions';
|
||||
export * from './components';
|
||||
export * from './functions';
|
||||
|
||||
import './reducer';
|
||||
import './middleware';
|
||||
|
||||
46
react/features/mobile/invite-search/actionTypes.js
Normal file
46
react/features/mobile/invite-search/actionTypes.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* The type of redux action to set InviteSearch's event subscriptions.
|
||||
*
|
||||
* {
|
||||
* type: _SET_INVITE_SEARCH_SUBSCRIPTIONS,
|
||||
* subscriptions: Array|undefined
|
||||
* }
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export const _SET_INVITE_SEARCH_SUBSCRIPTIONS
|
||||
= Symbol('_SET_INVITE_SEARCH_SUBSCRIPTIONS');
|
||||
|
||||
|
||||
/**
|
||||
* The type of the action which signals a request to launch the native invite
|
||||
* dialog.
|
||||
*
|
||||
* {
|
||||
* type: LAUNCH_NATIVE_INVITE
|
||||
* }
|
||||
*/
|
||||
export const LAUNCH_NATIVE_INVITE = Symbol('LAUNCH_NATIVE_INVITE');
|
||||
|
||||
/**
|
||||
* The type of the action which signals that native invites were sent
|
||||
* successfully.
|
||||
*
|
||||
* {
|
||||
* type: SEND_INVITE_SUCCESS,
|
||||
* inviteScope: string
|
||||
* }
|
||||
*/
|
||||
export const SEND_INVITE_SUCCESS = Symbol('SEND_INVITE_SUCCESS');
|
||||
|
||||
/**
|
||||
* The type of the action which signals that native invites failed to send
|
||||
* successfully.
|
||||
*
|
||||
* {
|
||||
* type: SEND_INVITE_FAILURE,
|
||||
* items: Array<*>,
|
||||
* inviteScope: string
|
||||
* }
|
||||
*/
|
||||
export const SEND_INVITE_FAILURE = Symbol('SEND_INVITE_FAILURE');
|
||||
50
react/features/mobile/invite-search/actions.js
Normal file
50
react/features/mobile/invite-search/actions.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
LAUNCH_NATIVE_INVITE,
|
||||
SEND_INVITE_SUCCESS,
|
||||
SEND_INVITE_FAILURE
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Launches the native invite dialog.
|
||||
*
|
||||
* @returns {{
|
||||
* type: LAUNCH_NATIVE_INVITE
|
||||
* }}
|
||||
*/
|
||||
export function launchNativeInvite() {
|
||||
return {
|
||||
type: LAUNCH_NATIVE_INVITE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that all native invites were sent successfully.
|
||||
*
|
||||
* @param {string} inviteScope - Scope identifier for the invite success. This
|
||||
* is used to look up relevant information on the native side.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function sendInviteSuccess(inviteScope: string) {
|
||||
return {
|
||||
type: SEND_INVITE_SUCCESS,
|
||||
inviteScope
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that some native invites failed to send successfully.
|
||||
*
|
||||
* @param {Array<*>} items - Invite items that failed to send.
|
||||
* @param {string} inviteScope - Scope identifier for the invite failure. This
|
||||
* is used to look up relevant information on the native side.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function sendInviteFailure(items: Array<*>, inviteScope: string) {
|
||||
return {
|
||||
type: SEND_INVITE_FAILURE,
|
||||
items,
|
||||
inviteScope
|
||||
};
|
||||
}
|
||||
5
react/features/mobile/invite-search/index.js
Normal file
5
react/features/mobile/invite-search/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
|
||||
import './reducer';
|
||||
import './middleware';
|
||||
233
react/features/mobile/invite-search/middleware.js
Normal file
233
react/features/mobile/invite-search/middleware.js
Normal file
@@ -0,0 +1,233 @@
|
||||
/* @flow */
|
||||
|
||||
import i18next from 'i18next';
|
||||
import { NativeModules, NativeEventEmitter } from 'react-native';
|
||||
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
|
||||
import { getInviteURL } from '../../base/connection';
|
||||
import {
|
||||
getInviteResultsForQuery,
|
||||
isAddPeopleEnabled,
|
||||
isDialOutEnabled,
|
||||
sendInvitesForItems
|
||||
} from '../../invite';
|
||||
import { inviteVideoRooms } from '../../videosipgw';
|
||||
|
||||
import { sendInviteSuccess, sendInviteFailure } from './actions';
|
||||
import {
|
||||
_SET_INVITE_SEARCH_SUBSCRIPTIONS,
|
||||
LAUNCH_NATIVE_INVITE,
|
||||
SEND_INVITE_SUCCESS,
|
||||
SEND_INVITE_FAILURE
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Middleware that captures Redux actions and uses the InviteSearch module to
|
||||
* turn them into native events so the application knows about them.
|
||||
*
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case APP_WILL_MOUNT:
|
||||
return _appWillMount(store, next, action);
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
store.dispatch({
|
||||
type: _SET_INVITE_SEARCH_SUBSCRIPTIONS,
|
||||
subscriptions: undefined
|
||||
});
|
||||
break;
|
||||
|
||||
case LAUNCH_NATIVE_INVITE:
|
||||
launchNativeInvite(store);
|
||||
break;
|
||||
|
||||
case SEND_INVITE_SUCCESS:
|
||||
onSendInviteSuccess(action);
|
||||
break;
|
||||
|
||||
case SEND_INVITE_FAILURE:
|
||||
onSendInviteFailure(action);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Notifies the feature jwt that the action {@link APP_WILL_MOUNT} is being
|
||||
* dispatched within a specific redux {@code store}.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is
|
||||
* being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
function _appWillMount({ dispatch, getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const emitter = new NativeEventEmitter(NativeModules.InviteSearch);
|
||||
|
||||
const context = {
|
||||
dispatch,
|
||||
getState
|
||||
};
|
||||
const subscriptions = [
|
||||
emitter.addListener(
|
||||
'performQueryAction',
|
||||
_onPerformQueryAction,
|
||||
context),
|
||||
emitter.addListener(
|
||||
'performSubmitInviteAction',
|
||||
_onPerformSubmitInviteAction,
|
||||
context)
|
||||
];
|
||||
|
||||
dispatch({
|
||||
type: _SET_INVITE_SEARCH_SUBSCRIPTIONS,
|
||||
subscriptions
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the native counterpart of InviteSearch to launch a native.
|
||||
* invite search.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function launchNativeInvite(store: { getState: Function }) {
|
||||
// The JavaScript App needs to provide uniquely identifying information
|
||||
// to the native module so that the latter may match the former
|
||||
// to the native JitsiMeetView which hosts it.
|
||||
const { app } = store.getState()['features/app'];
|
||||
|
||||
if (app) {
|
||||
const { externalAPIScope } = app.props;
|
||||
|
||||
if (externalAPIScope) {
|
||||
NativeModules.InviteSearch.launchNativeInvite(externalAPIScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the native counterpart of InviteSearch that all
|
||||
* invites were sent successfully.
|
||||
*
|
||||
* @param {Object} action - The redux action {@code SEND_INVITE_SUCCESS} which
|
||||
* is being dispatched.
|
||||
* @returns {void}
|
||||
*/
|
||||
function onSendInviteSuccess({ inviteScope }) {
|
||||
NativeModules.InviteSearch.inviteSucceeded(inviteScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the native counterpart of InviteSearch that some
|
||||
* invite items failed to send successfully.
|
||||
*
|
||||
* @param {Object} action - The redux action {@code SEND_INVITE_FAILURE} which
|
||||
* is being dispatched.
|
||||
* @returns {void}
|
||||
*/
|
||||
function onSendInviteFailure({ items, inviteScope }) {
|
||||
NativeModules.InviteSearch.inviteFailedForItems(items, inviteScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles InviteSearch's event {@code performQueryAction}.
|
||||
*
|
||||
* @param {Object} event - The details of the InviteSearch event
|
||||
* {@code performQueryAction}.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onPerformQueryAction({ query, inviteScope }) {
|
||||
const { getState } = this; // eslint-disable-line no-invalid-this
|
||||
|
||||
const state = getState();
|
||||
|
||||
const {
|
||||
dialOutAuthUrl,
|
||||
peopleSearchQueryTypes,
|
||||
peopleSearchUrl
|
||||
} = state['features/base/config'];
|
||||
|
||||
const options = {
|
||||
dialOutAuthUrl,
|
||||
enableAddPeople: isAddPeopleEnabled(state),
|
||||
enableDialOut: isDialOutEnabled(state),
|
||||
jwt: state['features/base/jwt'].jwt,
|
||||
peopleSearchQueryTypes,
|
||||
peopleSearchUrl
|
||||
};
|
||||
|
||||
getInviteResultsForQuery(query, options)
|
||||
.catch(() => [])
|
||||
.then(results => {
|
||||
const translatedResults = results.map(result => {
|
||||
if (result.type === 'phone') {
|
||||
result.title = i18next.t('addPeople.telephone', {
|
||||
number: result.number
|
||||
});
|
||||
|
||||
if (result.showCountryCodeReminder) {
|
||||
result.subtitle = i18next.t(
|
||||
'addPeople.countryReminder'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}).filter(result => result.type !== 'phone' || result.allowed);
|
||||
|
||||
NativeModules.InviteSearch.receivedResults(
|
||||
translatedResults,
|
||||
query,
|
||||
inviteScope);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles InviteSearch's event {@code performSubmitInviteAction}.
|
||||
*
|
||||
* @param {Object} event - The details of the InviteSearch event.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onPerformSubmitInviteAction({ selectedItems, inviteScope }) {
|
||||
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
const {
|
||||
inviteServiceUrl
|
||||
} = state['features/base/config'];
|
||||
const options = {
|
||||
conference,
|
||||
inviteServiceUrl,
|
||||
inviteUrl: getInviteURL(state),
|
||||
inviteVideoRooms,
|
||||
jwt: state['features/base/jwt'].jwt
|
||||
};
|
||||
|
||||
sendInvitesForItems(selectedItems, options)
|
||||
.then(invitesLeftToSend => {
|
||||
if (invitesLeftToSend.length) {
|
||||
dispatch(sendInviteFailure(invitesLeftToSend, inviteScope));
|
||||
} else {
|
||||
dispatch(sendInviteSuccess(inviteScope));
|
||||
}
|
||||
});
|
||||
}
|
||||
14
react/features/mobile/invite-search/reducer.js
Normal file
14
react/features/mobile/invite-search/reducer.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { assign, ReducerRegistry } from '../../base/redux';
|
||||
|
||||
import { _SET_INVITE_SEARCH_SUBSCRIPTIONS } from './actionTypes';
|
||||
|
||||
ReducerRegistry.register(
|
||||
'features/invite-search',
|
||||
(state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case _SET_INVITE_SEARCH_SUBSCRIPTIONS:
|
||||
return assign(state, 'subscriptions', action.subscriptions);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -14,6 +14,11 @@ import {
|
||||
isNarrowAspectRatio,
|
||||
makeAspectRatioAware
|
||||
} from '../../base/responsive-ui';
|
||||
import {
|
||||
InviteButton,
|
||||
isAddPeopleEnabled,
|
||||
isDialOutEnabled
|
||||
} from '../../invite';
|
||||
import {
|
||||
EnterPictureInPictureToolbarButton
|
||||
} from '../../mobile/picture-in-picture';
|
||||
@@ -39,7 +44,7 @@ import { AudioMuteButton, HangupButton, VideoMuteButton } from './buttons';
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
const _SHARE_ROOM_TOOLBAR_BUTTON = true;
|
||||
const _SHARE_ROOM_TOOLBAR_BUTTON = false;
|
||||
|
||||
/**
|
||||
* The type of {@link Toolbox}'s React {@code Component} props.
|
||||
@@ -56,6 +61,18 @@ type Props = {
|
||||
*/
|
||||
_audioOnly: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the feature to directly invite people into the
|
||||
* conference is available.
|
||||
*/
|
||||
_enableAddPeople: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the feature to dial out to number to join the
|
||||
* conference is available.
|
||||
*/
|
||||
_enableDialOut: boolean,
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the toolbox is enabled.
|
||||
*/
|
||||
@@ -212,9 +229,13 @@ class Toolbox extends Component<Props> {
|
||||
const underlayColor = 'transparent';
|
||||
const {
|
||||
_audioOnly: audioOnly,
|
||||
_enableAddPeople: enableAddPeople,
|
||||
_enableDialOut: enableDialOut,
|
||||
_videoMuted: videoMuted
|
||||
} = this.props;
|
||||
|
||||
const showInviteButton = enableAddPeople || enableDialOut;
|
||||
|
||||
/* eslint-disable react/jsx-curly-spacing,react/jsx-handler-names */
|
||||
|
||||
return (
|
||||
@@ -252,7 +273,7 @@ class Toolbox extends Component<Props> {
|
||||
style = { style }
|
||||
underlayColor = { underlayColor } />
|
||||
{
|
||||
_SHARE_ROOM_TOOLBAR_BUTTON
|
||||
_SHARE_ROOM_TOOLBAR_BUTTON && !showInviteButton
|
||||
&& <ToolbarButton
|
||||
iconName = 'link'
|
||||
iconStyle = { iconStyle }
|
||||
@@ -260,6 +281,15 @@ class Toolbox extends Component<Props> {
|
||||
style = { style }
|
||||
underlayColor = { underlayColor } />
|
||||
}
|
||||
{
|
||||
showInviteButton
|
||||
&& <InviteButton
|
||||
enableAddPeople = { enableAddPeople }
|
||||
enableDialOut = { enableDialOut }
|
||||
iconStyle = { iconStyle }
|
||||
style = { style }
|
||||
underlayColor = { underlayColor } />
|
||||
}
|
||||
<EnterPictureInPictureToolbarButton
|
||||
iconStyle = { iconStyle }
|
||||
style = { style }
|
||||
@@ -388,6 +418,22 @@ function _mapStateToProps(state) {
|
||||
*/
|
||||
_audioOnly: Boolean(conference.audioOnly),
|
||||
|
||||
/**
|
||||
* Whether or not the feature to directly invite people into the
|
||||
* conference is available.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
_enableAddPeople: isAddPeopleEnabled(state),
|
||||
|
||||
/**
|
||||
* Whether or not the feature to dial out to number to join the
|
||||
* conference is available.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
_enableDialOut: isDialOutEnabled(state),
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the toolbox is enabled.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user