diff --git a/config.js b/config.js
index 6380b0a70a..a14f348adf 100644
--- a/config.js
+++ b/config.js
@@ -1549,6 +1549,17 @@ var config = {
// and will automatically redirect to the token service to get the token for the meeting.
// tokenAuthUrlAutoRedirect: false
+ // You can put an array of values to target different entity types in the invite dialog.
+ // Valid values are "phone", "room", "sip", "user", "videosipgw" and "email"
+ // peopleSearchQueryTypes: ["user", "email"],
+ // Directory endpoint which is called for invite dialog autocomplete
+ // peopleSearchUrl: "https://myservice.com/api/people",
+ // Endpoint which is called to send invitation requests
+ // inviteServiceUrl: "https://myservice.com/api/invite",
+
+ // For external entities (e. g. email), the localStorage key holding the token value for directory authentication
+ // peopleSearchTokenLocation: "mytoken",
+
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold
@@ -1565,8 +1576,6 @@ var config = {
iAmRecorder
iAmSipGateway
microsoftApiApplicationClientID
- peopleSearchQueryTypes
- peopleSearchUrl
requireDisplayName
*/
diff --git a/react/features/base/avatar/components/Avatar.tsx b/react/features/base/avatar/components/Avatar.tsx
index 7aaf187cb6..3cd131bcc6 100644
--- a/react/features/base/avatar/components/Avatar.tsx
+++ b/react/features/base/avatar/components/Avatar.tsx
@@ -48,6 +48,11 @@ export interface IProps {
*/
colorBase?: string;
+ /**
+ * Indicates the default icon for the avatar.
+ */
+ defaultIcon?: string;
+
/**
* Display name of the entity to render an avatar for (if any). This is handy when we need
* an avatar for a non-participant entity (e.g. A recent list item).
@@ -112,6 +117,7 @@ class Avatar
extends PureComponent
{
* @static
*/
static defaultProps = {
+ defaultIcon: IconUser,
dynamicColor: true
};
@@ -172,6 +178,7 @@ class Avatar
extends PureComponent
{
_loadableAvatarUrlUseCORS,
className,
colorBase,
+ defaultIcon,
dynamicColor,
id,
size,
@@ -229,7 +236,7 @@ class Avatar
extends PureComponent
{
}
if (navigator.product !== 'ReactNative') {
- avatarProps.iconUser = IconUser;
+ avatarProps.iconUser = defaultIcon;
}
return (
diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts
index 94523f9f8f..52386194e3 100644
--- a/react/features/base/config/configType.ts
+++ b/react/features/base/config/configType.ts
@@ -477,6 +477,7 @@ export interface IConfig {
};
pcStatsInterval?: number;
peopleSearchQueryTypes?: string[];
+ peopleSearchTokenLocation?: string;
peopleSearchUrl?: string;
preferBosh?: boolean;
preferVisitor?: boolean;
diff --git a/react/features/invite/actions.any.ts b/react/features/invite/actions.any.ts
index 315c79bc43..43bd901992 100644
--- a/react/features/invite/actions.any.ts
+++ b/react/features/invite/actions.any.ts
@@ -82,14 +82,19 @@ export function invite(
const { conference, password } = state['features/base/conference'];
if (typeof conference === 'undefined') {
+ // Only keep invitees which can get an invite request from Jitsi UI
+ const jitsiInvitees = invitees.filter(({ type }) => type !== INVITE_TYPES.EMAIL);
+
// Invite will fail before CONFERENCE_JOIN. The request will be
// cached in order to be executed on CONFERENCE_JOIN.
- return new Promise(resolve => {
- dispatch(addPendingInviteRequest({
- invitees,
- callback: (failedInvitees: any) => resolve(failedInvitees)
- }));
- });
+ if (jitsiInvitees.length) {
+ return new Promise(resolve => {
+ dispatch(addPendingInviteRequest({
+ invitees: jitsiInvitees,
+ callback: (failedInvitees: any) => resolve(failedInvitees)
+ }));
+ });
+ }
}
let allInvitePromises: Promise[] = [];
@@ -112,10 +117,12 @@ export function invite(
// For each number, dial out. On success, remove the number from
// {@link invitesLeftToSend}.
- const phoneInvitePromises = phoneNumbers.map(item => {
- const numberToInvite = item.number;
+ const phoneInvitePromises = typeof conference === 'undefined'
+ ? []
+ : phoneNumbers.map(item => {
+ const numberToInvite = item.number;
- return conference.dial(numberToInvite)
+ return conference.dial(numberToInvite)
.then(() => {
invitesLeftToSend
= invitesLeftToSend.filter(
@@ -123,13 +130,13 @@ export function invite(
})
.catch((error: Error) =>
logger.error('Error inviting phone number:', error));
- });
+ });
allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
const usersAndRooms
= invitesLeftToSend.filter(
- ({ type }) => [ INVITE_TYPES.USER, INVITE_TYPES.ROOM ].includes(type));
+ ({ type }) => [ INVITE_TYPES.USER, INVITE_TYPES.EMAIL, INVITE_TYPES.ROOM ].includes(type));
if (usersAndRooms.length) {
// Send a request to invite all the rooms and users. On success,
@@ -139,12 +146,12 @@ export function invite(
(callFlowsEnabled
? inviteServiceCallFlowsUrl : inviteServiceUrl) ?? '',
inviteUrl,
- jwt,
- usersAndRooms)
+ usersAndRooms,
+ state)
.then(() => {
invitesLeftToSend
= invitesLeftToSend.filter(
- ({ type }) => ![ INVITE_TYPES.USER, INVITE_TYPES.ROOM ].includes(type));
+ ({ type }) => ![ INVITE_TYPES.USER, INVITE_TYPES.EMAIL, INVITE_TYPES.ROOM ].includes(type));
})
.catch(error => {
dispatch(setCalleeInfoVisible(false));
diff --git a/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.tsx b/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.tsx
index e70d0ff7f7..98b0fd8f82 100644
--- a/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.tsx
+++ b/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.tsx
@@ -61,6 +61,11 @@ export interface IProps {
*/
_peopleSearchQueryTypes: Array;
+ /**
+ * The localStorage key holding the alternative token for people directory.
+ */
+ _peopleSearchTokenLocation: string;
+
/**
* The URL pointing to the service allowing for people search.
*/
@@ -254,6 +259,7 @@ export default class AbstractAddPeopleDialog
_jwt: jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl,
+ _peopleSearchTokenLocation: peopleSearchTokenLocation,
_region: region,
_sipInviteEnabled: sipInviteEnabled
} = this.props;
@@ -266,6 +272,7 @@ export default class AbstractAddPeopleDialog
jwt,
peopleSearchQueryTypes,
peopleSearchUrl,
+ peopleSearchTokenLocation,
region,
sipInviteEnabled
};
@@ -295,7 +302,8 @@ export function _mapStateToProps(state: IReduxState) {
dialOutAuthUrl,
dialOutRegionUrl,
peopleSearchQueryTypes,
- peopleSearchUrl
+ peopleSearchUrl,
+ peopleSearchTokenLocation
} = state['features/base/config'];
return {
@@ -308,6 +316,7 @@ export function _mapStateToProps(state: IReduxState) {
_jwt: state['features/base/jwt'].jwt ?? '',
_peopleSearchQueryTypes: peopleSearchQueryTypes ?? [],
_peopleSearchUrl: peopleSearchUrl ?? '',
+ _peopleSearchTokenLocation: peopleSearchTokenLocation ?? '',
_region: getMeetingRegion(state),
_sipInviteEnabled: isSipInviteEnabled(state)
};
diff --git a/react/features/invite/components/add-people-dialog/native/AddPeopleDialog.tsx b/react/features/invite/components/add-people-dialog/native/AddPeopleDialog.tsx
index 13eb042cca..6a88365a4f 100644
--- a/react/features/invite/components/add-people-dialog/native/AddPeopleDialog.tsx
+++ b/react/features/invite/components/add-people-dialog/native/AddPeopleDialog.tsx
@@ -19,6 +19,7 @@ import Icon from '../../../../base/icons/components/Icon';
import {
IconCheck,
IconCloseCircle,
+ IconEnvelope,
IconPhoneRinging,
IconSearch,
IconShare
@@ -260,6 +261,12 @@ class AddPeopleDialog extends AbstractAddPeopleDialog {
key: item.id || item.user_id,
title: item.name
};
+ case INVITE_TYPES.EMAIL:
+ return {
+ avatar: item.avatar || IconEnvelope,
+ key: item.id || item.user_id,
+ title: item.name
+ };
default:
return null;
}
@@ -273,7 +280,11 @@ class AddPeopleDialog extends AbstractAddPeopleDialog {
* @returns {string}
*/
_keyExtractor(item: any) {
- return item.type === INVITE_TYPES.USER ? item.id || item.user_id : item.number;
+ if (item.type === INVITE_TYPES.USER || item.type === INVITE_TYPES.EMAIL) {
+ return item.id || item.user_id;
+ }
+
+ return item.number;
}
/**
@@ -451,6 +462,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog {
selected = inviteItems.find(_.matchesProperty('number', item.number));
break;
case INVITE_TYPES.USER:
+ case INVITE_TYPES.EMAIL:
selected = item.id
? inviteItems.find(_.matchesProperty('id', item.id))
: inviteItems.find(_.matchesProperty('user_id', item.user_id));
diff --git a/react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx b/react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx
index 5b68c760a0..6482eaa3c2 100644
--- a/react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx
+++ b/react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx
@@ -7,7 +7,7 @@ import { IReduxState, IStore } from '../../../../app/types';
import Avatar from '../../../../base/avatar/components/Avatar';
import { translate } from '../../../../base/i18n/functions';
import Icon from '../../../../base/icons/components/Icon';
-import { IconPhoneRinging } from '../../../../base/icons/svg';
+import { IconEnvelope, IconPhoneRinging, IconUser } from '../../../../base/icons/svg';
import MultiSelectAutocomplete from '../../../../base/react/components/web/MultiSelectAutocomplete';
import Button from '../../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../../base/ui/constants.any';
@@ -302,9 +302,12 @@ class InviteContactsForm extends AbstractAddPeopleDialog {
* @returns {ReactElement}
*/
_getAvatar(user: any, className = 'avatar-small') {
+ const defaultIcon = user.type === INVITE_TYPES.EMAIL ? IconEnvelope : IconUser;
+
return (
@@ -325,7 +328,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog {
_parseQueryResults(response: IInvitee[] = []) {
const { t, _dialOutEnabled } = this.props;
- const userTypes = [ INVITE_TYPES.USER, INVITE_TYPES.VIDEO_ROOM, INVITE_TYPES.ROOM ];
+ const userTypes = [ INVITE_TYPES.USER, INVITE_TYPES.EMAIL, INVITE_TYPES.VIDEO_ROOM, INVITE_TYPES.ROOM ];
const users = response.filter(item => userTypes.includes(item.type));
const userDisplayItems: any = [];
diff --git a/react/features/invite/constants.ts b/react/features/invite/constants.ts
index a4b6c31078..0ad03482f9 100644
--- a/react/features/invite/constants.ts
+++ b/react/features/invite/constants.ts
@@ -48,6 +48,7 @@ export const SIP_ADDRESS_REGEX = /^[+a-zA-Z0-9]+(?:([^\s>:@]+)(?::([^\s@>]+))?@)
* Different invite types mapping.
*/
export const INVITE_TYPES = {
+ EMAIL: 'email',
PHONE: 'phone',
ROOM: 'room',
SIP: 'sip',
diff --git a/react/features/invite/functions.ts b/react/features/invite/functions.ts
index 44147ba3a6..872b6bdba8 100644
--- a/react/features/invite/functions.ts
+++ b/react/features/invite/functions.ts
@@ -1,3 +1,6 @@
+// @ts-expect-error
+import { jitsiLocalStorage } from '@jitsi/js-utils';
+
import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types';
import { getRoomName } from '../base/conference/functions';
@@ -143,6 +146,11 @@ export type GetInviteResultsOptions = {
*/
peopleSearchQueryTypes: Array;
+ /**
+ * Key in localStorage holding the alternative token for people directory.
+ */
+ peopleSearchTokenLocation?: string;
+
/**
* The url to query for people.
*/
@@ -181,6 +189,7 @@ export function getInviteResultsForQuery(
dialOutEnabled,
peopleSearchQueryTypes,
peopleSearchUrl,
+ peopleSearchTokenLocation,
region,
sipInviteEnabled,
jwt
@@ -193,7 +202,8 @@ export function getInviteResultsForQuery(
peopleSearchUrl,
jwt,
text,
- peopleSearchQueryTypes);
+ peopleSearchQueryTypes,
+ peopleSearchTokenLocation);
} else {
peopleSearchPromise = Promise.resolve([]);
}
@@ -411,29 +421,40 @@ export function getInviteTypeCounts(inviteItems: IInvitee[] = []) {
* @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.
+ * @param {IReduxState} state - Global state.
* @returns {Promise} - The promise created by the request.
*/
export function invitePeopleAndChatRooms(
inviteServiceUrl: string,
inviteUrl: string,
- jwt: string,
- inviteItems: Array
+ inviteItems: Array,
+ state: IReduxState
): Promise {
if (!inviteItems || inviteItems.length === 0) {
return Promise.resolve();
}
+ // Parse all the query strings of the search directory endpoint
+ const { jwt = '' } = state['features/base/jwt'];
+ const { peopleSearchTokenLocation } = state['features/base/config'];
+
+ let token = jwt;
+
+ // If token is empty, check for alternate token
+ if (!token && peopleSearchTokenLocation) {
+ token = jitsiLocalStorage.getItem(peopleSearchTokenLocation) ?? '';
+ }
+
const headers = {
- ...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
+ ...token ? { 'Authorization': `Bearer ${token}` } : {},
'Content-Type': 'application/json'
};
return fetch(
- `${inviteServiceUrl}`,
+ inviteServiceUrl,
{
body: JSON.stringify({
'invited': inviteItems,
@@ -452,9 +473,14 @@ export function invitePeopleAndChatRooms(
* @returns {boolean} Indication of whether adding people is currently enabled.
*/
export function isAddPeopleEnabled(state: IReduxState): boolean {
- const { peopleSearchUrl } = state['features/base/config'];
+ const {
+ peopleSearchUrl,
+ peopleSearchTokenLocation
+ } = state['features/base/config'];
- return Boolean(state['features/base/jwt'].jwt && Boolean(peopleSearchUrl) && !isVpaasMeeting(state));
+ const hasToken = Boolean(state['features/base/jwt'].jwt || Boolean(peopleSearchTokenLocation));
+
+ return Boolean(hasToken && Boolean(peopleSearchUrl) && !isVpaasMeeting(state));
}
/**
@@ -534,21 +560,30 @@ function isPhoneNumberRegex(): RegExp {
* @param {string} jwt - The jwt token to pass to the search service.
* @param {string} text - Text to search.
* @param {Array} queryTypes - Array with the query types that will be
- * executed - "conferenceRooms" | "user" | "room".
+ * executed - "conferenceRooms" | "user" | "room" | "email".
+ * @param {string} peopleSearchTokenLocation - The localStorage key holding the token value for alternate auth.
* @returns {Promise} - The promise created by the request.
*/
export function searchDirectory( // eslint-disable-line max-params
serviceUrl: string,
jwt: string,
text: string,
- queryTypes: Array = [ 'conferenceRooms', 'user', 'room' ]
+ queryTypes: Array = [ 'conferenceRooms', 'user', 'room', 'email' ],
+ peopleSearchTokenLocation?: string
): Promise> {
const query = encodeURIComponent(text);
const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
+ let token = jwt;
+
+ // If token is empty, check for alternate token
+ if (!token && peopleSearchTokenLocation) {
+ token = jitsiLocalStorage.getItem(peopleSearchTokenLocation) ?? '';
+ }
+
const headers = {
- ...jwt ? { 'Authorization': `Bearer ${jwt}` } : {}
+ ...token ? { 'Authorization': `Bearer ${token}` } : {}
};
return fetch(`${serviceUrl}?query=${query}&queryTypes=${