mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 11:22:31 +00:00
feat(invite) add email autocomplete in invite (#14610)
* feat(invite) add email value in peopleSearchQueryTypes and peopleSearchToken config * feat(invite) pass custom auth token in search directory (e. g. for email directory) * feat(invite) autocomplete and invitations working with custom auth token (e. g. email invite type), invite icons updated * feat(invite) remove newly documented config from undocumented settings list * feat(invite) jwt are now passed in the invite requests headers * feat(invite) linter-related formatting * feat(invite) fix default user icon regression * feat(invite) last lint issues * feat(invite) pass alternate token in header, not in params * Fixes lint error --------- Co-authored-by: Raphaël Badawi <raphael.badawi@ceo-vision.com> Co-authored-by: Дамян Минков <damencho@jitsi.org>
This commit is contained in:
13
config.js
13
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
|
||||
*/
|
||||
|
||||
|
||||
@@ -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<P extends IProps> extends PureComponent<P, IState> {
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
defaultIcon: IconUser,
|
||||
dynamicColor: true
|
||||
};
|
||||
|
||||
@@ -172,6 +178,7 @@ class Avatar<P extends IProps> extends PureComponent<P, IState> {
|
||||
_loadableAvatarUrlUseCORS,
|
||||
className,
|
||||
colorBase,
|
||||
defaultIcon,
|
||||
dynamicColor,
|
||||
id,
|
||||
size,
|
||||
@@ -229,7 +236,7 @@ class Avatar<P extends IProps> extends PureComponent<P, IState> {
|
||||
}
|
||||
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
avatarProps.iconUser = IconUser;
|
||||
avatarProps.iconUser = defaultIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -477,6 +477,7 @@ export interface IConfig {
|
||||
};
|
||||
pcStatsInterval?: number;
|
||||
peopleSearchQueryTypes?: string[];
|
||||
peopleSearchTokenLocation?: string;
|
||||
peopleSearchUrl?: string;
|
||||
preferBosh?: boolean;
|
||||
preferVisitor?: boolean;
|
||||
|
||||
@@ -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<any>[] = [];
|
||||
@@ -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));
|
||||
|
||||
@@ -61,6 +61,11 @@ export interface IProps {
|
||||
*/
|
||||
_peopleSearchQueryTypes: Array<string>;
|
||||
|
||||
/**
|
||||
* 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<P extends IProps, S extends IState>
|
||||
_jwt: jwt,
|
||||
_peopleSearchQueryTypes: peopleSearchQueryTypes,
|
||||
_peopleSearchUrl: peopleSearchUrl,
|
||||
_peopleSearchTokenLocation: peopleSearchTokenLocation,
|
||||
_region: region,
|
||||
_sipInviteEnabled: sipInviteEnabled
|
||||
} = this.props;
|
||||
@@ -266,6 +272,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
|
||||
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)
|
||||
};
|
||||
|
||||
@@ -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<IProps, IState> {
|
||||
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<IProps, IState> {
|
||||
* @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<IProps, IState> {
|
||||
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));
|
||||
|
||||
@@ -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<IProps, IState> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_getAvatar(user: any, className = 'avatar-small') {
|
||||
const defaultIcon = user.type === INVITE_TYPES.EMAIL ? IconEnvelope : IconUser;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
className = { className }
|
||||
defaultIcon = { defaultIcon }
|
||||
size = { 32 }
|
||||
status = { user.status }
|
||||
url = { user.avatar } />
|
||||
@@ -325,7 +328,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<IProps, IState> {
|
||||
_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 = [];
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<string>;
|
||||
|
||||
/**
|
||||
* 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<Object>
|
||||
inviteItems: Array<Object>,
|
||||
state: IReduxState
|
||||
): Promise<any> {
|
||||
|
||||
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<string>} 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<string> = [ 'conferenceRooms', 'user', 'room' ]
|
||||
queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room', 'email' ],
|
||||
peopleSearchTokenLocation?: string
|
||||
): Promise<Array<{ type: string; }>> {
|
||||
|
||||
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=${
|
||||
|
||||
Reference in New Issue
Block a user