[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:
Ryan Peck
2018-03-27 19:49:22 -05:00
committed by Saúl Ibarra Corretgé
parent 4e36127dc7
commit f64c13d4b7
24 changed files with 1559 additions and 185 deletions

View File

@@ -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

View File

@@ -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);
}
/**

View 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);

View File

@@ -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);
}

View File

@@ -1,5 +1,6 @@
export * from './actions';
export * from './components';
export * from './functions';
import './reducer';
import './middleware';

View 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');

View 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
};
}

View File

@@ -0,0 +1,5 @@
export * from './actions';
export * from './actionTypes';
import './reducer';
import './middleware';

View 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));
}
});
}

View 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;
});

View File

@@ -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.
*