ref(invite): add people form (#13207)

This commit is contained in:
Gabriel Borlea
2023-04-20 14:00:42 +03:00
committed by GitHub
parent bf1dde7cd1
commit aec86cecc0
15 changed files with 454 additions and 454 deletions

View File

@@ -60,7 +60,6 @@ $flagsImagePath: "../images/";
@import 'filmstrip/vertical_filmstrip';
@import 'filmstrip/vertical_filmstrip_overrides';
@import 'unsupported-browser/main';
@import 'modals/invite/add-people';
@import 'deep-linking/main';
@import 'transcription-subtitles';
@import '_meetings_list.scss';

View File

@@ -12,24 +12,6 @@
}
}
/**
* Styling inline dialog errors.
*/
.inline-dialog-error {
margin-top: 16px;
&-text {
color: $dialogErrorText;
margin-bottom: 8px;
text-align: center;
}
&-button {
display: block;
margin: 16px auto 0 auto;
}
}
/**
* Styling shared video dialog errors.
*/

View File

@@ -41,10 +41,3 @@
}
}
}
/**
* Styles errors in the MultiSelectAutocomplete.
*/
.autocomplete-error {
min-width: 260px;
}

View File

@@ -1,57 +1,4 @@
.invite-more {
&-container {
margin-bottom: 8px;
transition: margin-bottom 0.3s;
&.elevated {
margin-bottom: 36px;
}
}
&-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
background: rgba(0, 0, 0, 0.7);
border-radius: 8px;
color: #fff;
font-size: 14px;
line-height: 24px;
font-weight: 600;
}
&-header {
max-width: 100%;
margin-bottom: 16px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
&-button {
display: flex;
max-width: 100%;
height: 40px;
box-sizing: border-box;
padding: 8px 16px;
background: #0376DA;
border-radius: 3px;
cursor: pointer;
@media (hover: hover) and (pointer: fine) {
&:hover {
background: #278ADF;
}
}
&-text {
margin-left: 8px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
&-dialog {
color: #fff;
font-size: 15px;
@@ -65,59 +12,6 @@
background: #5E6D7A;
}
&.email-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 8px 8px 16px;
margin-top: 24px;
width: calc(100% - 26px);
height: 22px;
background: #2A3A4B;
border: 1px solid #5E6D7A;
border-radius: 3px;
cursor: pointer;
&.active {
border-radius: 3px 3px 0 0;
}
}
&.invite-buttons {
width: 100%;
text-align: right;
margin-top: 8px;
& > a {
display: inline-block;
height: 24px;
min-width: 48px;
border-radius: 3px;
text-align: center;
text-decoration: none;
cursor: pointer;
}
&-cancel {
margin-right: 16px;
padding: 7px 15px;
background: #2A3A4B;
border: 1px solid #5E6D7A;
}
&-add {
padding: 8px 16px;
background: #0376DA;
}
&.disabled {
& > a {
pointer-events: none;
}
}
}
&.stream {
display: flex;
justify-content: space-between;
@@ -158,14 +52,3 @@
}
}
}
.mobile-browser {
.invite-more-content {
font-size: 16px;
}
.invite-more-button {
height: 48px;
padding: 12px 16px;
}
}

View File

@@ -1,13 +1,31 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import { translate } from '../../../i18n/functions';
import { withPixelLineHeight } from '../../../styles/functions.web';
import Button from '../../../ui/components/web/Button';
const useStyles = makeStyles()(theme => {
return {
dialog: {
backgroundColor: theme.palette.ui01,
border: `1px solid ${theme.palette.ui04}`,
borderRadius: `${Number(theme.shape.borderRadius)}px`,
boxShadow: '0px 1px 2px rgba(41, 41, 41, 0.25)',
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.bodyShortRegular),
padding: `${theme.spacing(3)} 10`,
'& .retry-button': {
margin: '16px auto 0 auto'
}
}
};
});
/**
* The type of the React {@code Component} props of {@link InlineDialogFailure}.
*/
interface IProps extends WithTranslation {
interface IProps {
/**
* Allows to retry the call that previously didn't succeed.
@@ -22,50 +40,48 @@ interface IProps extends WithTranslation {
/**
* Inline dialog that represents a failure and allows a retry.
*
* @returns {Element}
*/
class InlineDialogFailure extends Component<IProps> {
/**
* Renders the content of this component.
*
* @returns {ReactElement}
*/
render() {
const { t, showSupportLink } = this.props;
const InlineDialogFailure = ({
onRetry,
showSupportLink
}: IProps) => {
const { t } = useTranslation();
const { classes } = useStyles();
const supportLink = interfaceConfig.SUPPORT_URL;
const supportString = t('inlineDialogFailure.supportMsg');
const supportLinkElem
= supportLink && showSupportLink
? (
<div className = 'inline-dialog-error-text'>
<span>{ supportString.padEnd(supportString.length + 1) }
</span>
<span>
<a
href = { supportLink }
rel = 'noopener noreferrer'
target = '_blank'>
{ t('inlineDialogFailure.support') }
</a>
</span>
<span>.</span>
</div>
)
: null;
return (
<div className = 'inline-dialog-error'>
<div className = 'inline-dialog-error-text'>
{ t('inlineDialogFailure.msg') }
</div>
{ supportLinkElem }
<Button
className = 'inline-dialog-error-button'
label = { t('inlineDialogFailure.retry') }
onClick = { this.props.onRetry } />
const supportLink = interfaceConfig.SUPPORT_URL;
const supportString = t('inlineDialogFailure.supportMsg');
const supportLinkElem = supportLink && showSupportLink
? (
<div>
<span>{ supportString.padEnd(supportString.length + 1) }
</span>
<span>
<a
href = { supportLink }
rel = 'noopener noreferrer'
target = '_blank'>
{ t('inlineDialogFailure.support') }
</a>
</span>
<span>.</span>
</div>
);
}
}
)
: null;
export default translate(InlineDialogFailure);
return (
<div className = { classes.dialog }>
<div>
{ t('inlineDialogFailure.msg') }
</div>
{ supportLinkElem }
<Button
className = 'retry-button'
label = { t('inlineDialogFailure.retry') }
onClick = { onRetry } />
</div>
);
};
export default InlineDialogFailure;

View File

@@ -1,10 +1,8 @@
// @flow
import AKInlineDialog from '@atlaskit/inline-dialog';
import { MultiSelectStateless } from '@atlaskit/multi-select';
import _debounce from 'lodash/debounce';
import React, { Component } from 'react';
import { MultiSelectItem } from '../../../ui/components/types';
import MultiSelect from '../../../ui/components/web/MultiSelect';
import logger from '../../logger';
import InlineDialogFailure from './InlineDialogFailure';
@@ -13,119 +11,119 @@ import InlineDialogFailure from './InlineDialogFailure';
* The type of the React {@code Component} props of
* {@link MultiSelectAutocomplete}.
*/
type Props = {
interface IProps {
/**
* The default value of the selected item.
*/
defaultValue: Array<Object>,
defaultValue?: Array<any>;
/**
* Optional footer to show as a last element in the results.
* Should be of type {content: <some content>}.
*/
footer: Object,
footer?: any;
/**
* Indicates if the component is disabled.
*/
isDisabled: boolean,
isDisabled: boolean;
/**
* Text to display while a query is executing.
*/
loadingMessage: string,
loadingMessage: string;
/**
* The text to show when no matches are found.
*/
noMatchesFound: string,
noMatchesFound: string;
/**
* The function called immediately before a selection has been actually
* selected. Provides an opportunity to do any formatting.
*/
onItemSelected: Function,
onItemSelected: Function;
/**
* The function called when the selection changes.
*/
onSelectionChange: Function,
onSelectionChange: Function;
/**
* The placeholder text of the input component.
*/
placeholder: string,
placeholder: string;
/**
* The service providing the search.
*/
resourceClient: { makeQuery: Function, parseResults: Function },
resourceClient: { makeQuery: Function; parseResults: Function; };
/**
* Indicates if the component should fit the container.
*/
shouldFitContainer: boolean,
shouldFitContainer: boolean;
/**
* Indicates if we should focus.
*/
shouldFocus: boolean,
shouldFocus: boolean;
/**
* Indicates whether the support link should be shown in case of an error.
*/
showSupportLink: Boolean,
};
showSupportLink: Boolean;
}
/**
* The type of the React {@code Component} state of
* {@link MultiSelectAutocomplete}.
*/
type State = {
/**
* Indicates if the dropdown is open.
*/
isOpen: boolean,
/**
* The text that filters the query result of the search.
*/
filterValue: string,
/**
* Indicates if the component is currently loading results.
*/
loading: boolean,
interface IState {
/**
* Indicates if there was an error.
*/
error: boolean,
error: boolean;
/**
* The text that filters the query result of the search.
*/
filterValue: string;
/**
* Indicates if the dropdown is open.
*/
isOpen: boolean;
/**
* The list of result items.
*/
items: Array<Object>,
items: Array<MultiSelectItem>;
/**
* Indicates if the component is currently loading results.
*/
loading: boolean;
/**
* The list of selected items.
*/
selectedItems: Array<Object>
};
selectedItems: Array<MultiSelectItem>;
}
/**
* A MultiSelect that is also auto-completing.
*/
class MultiSelectAutocomplete extends Component<Props, State> {
class MultiSelectAutocomplete extends Component<IProps, IState> {
/**
* Initializes a new {@code MultiSelectAutocomplete} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
const defaultValue = this.props.defaultValue || [];
@@ -148,11 +146,11 @@ class MultiSelectAutocomplete extends Component<Props, State> {
/**
* Sets the items to display as selected.
*
* @param {Array<Object>} selectedItems - The list of items to display as
* @param {Array<MultiSelectItem>} selectedItems - The list of items to display as
* having been selected.
* @returns {void}
*/
setSelectedItems(selectedItems: Array<Object> = []) {
setSelectedItems(selectedItems: Array<MultiSelectItem> = []) {
this.setState({ selectedItems });
}
@@ -162,37 +160,32 @@ class MultiSelectAutocomplete extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const shouldFitContainer = this.props.shouldFitContainer || false;
const shouldFocus = this.props.shouldFocus || false;
const isDisabled = this.props.isDisabled || false;
const autoFocus = this.props.shouldFocus || false;
const disabled = this.props.isDisabled || false;
const placeholder = this.props.placeholder || '';
const noMatchesFound = this.props.noMatchesFound || '';
const errorDialog = this._renderError();
return (
<div>
<MultiSelectStateless
<MultiSelect
autoFocus = { autoFocus }
disabled = { disabled }
error = { this.state.error }
errorDialog = { errorDialog }
filterValue = { this.state.filterValue }
footer = { this.props.footer }
icon = { null }
isDisabled = { isDisabled }
isLoading = { this.state.loading }
isOpen = { this.state.isOpen }
items = { this.state.items }
loadingMessage = { this.props.loadingMessage }
noMatchesFound = { noMatchesFound }
noMatchesText = { noMatchesFound }
onFilterChange = { this._onFilterChange }
onRemoved = { this._onSelectionChange }
onSelected = { this._onSelectionChange }
placeholder = { placeholder }
selectedItems = { this.state.selectedItems }
shouldFitContainer = { shouldFitContainer }
shouldFocus = { shouldFocus } />
{ this._renderError() }
selectedItems = { this.state.selectedItems } />
</div>
);
}
_onFilterChange: (string) => void;
/**
* Sets the state and sends a query on filter change.
@@ -201,7 +194,7 @@ class MultiSelectAutocomplete extends Component<Props, State> {
* @private
* @returns {void}
*/
_onFilterChange(filterValue) {
_onFilterChange(filterValue: string) {
this.setState({
// Clean the error if the filterValue is empty.
error: this.state.error && Boolean(filterValue),
@@ -215,8 +208,6 @@ class MultiSelectAutocomplete extends Component<Props, State> {
}
}
_onRetry: () => void;
/**
* Retries the query on retry.
*
@@ -227,18 +218,16 @@ class MultiSelectAutocomplete extends Component<Props, State> {
this._sendQuery(this.state.filterValue);
}
_onSelectionChange: (Object) => void;
/**
* Updates the selected items when a selection event occurs.
*
* @param {Object} item - The selected item.
* @param {any} item - The selected item.
* @private
* @returns {void}
*/
_onSelectionChange(item) {
_onSelectionChange(item: any) {
const existing
= this.state.selectedItems.find(k => k.value === item.value);
= this.state.selectedItems.find((k: any) => k.value === item.value);
let selectedItems = this.state.selectedItems;
if (existing) {
@@ -265,30 +254,22 @@ class MultiSelectAutocomplete extends Component<Props, State> {
if (!this.state.error) {
return null;
}
const content = (
<div className = 'autocomplete-error'>
<InlineDialogFailure
onRetry = { this._onRetry }
showSupportLink = { this.props.showSupportLink } />
</div>
);
return (
<AKInlineDialog
content = { content }
isOpen = { true } />
<InlineDialogFailure
onRetry = { this._onRetry }
showSupportLink = { this.props.showSupportLink } />
);
}
_sendQuery: (string) => void;
/**
* Sends a query to the resourceClient.
*
* @param {string} filterValue - The string to use for the search.
* @returns {void}
*/
_sendQuery(filterValue) {
_sendQuery(filterValue: string) {
if (!filterValue) {
return;
}
@@ -299,11 +280,11 @@ class MultiSelectAutocomplete extends Component<Props, State> {
const resourceClient = this.props.resourceClient || {
makeQuery: () => Promise.resolve([]),
parseResults: results => results
parseResults: (results: any) => results
};
resourceClient.makeQuery(filterValue)
.then(results => {
.then((results: any) => {
if (this.state.filterValue !== filterValue) {
this.setState({
error: false
@@ -311,20 +292,15 @@ class MultiSelectAutocomplete extends Component<Props, State> {
return;
}
const itemGroups = [
{
items: resourceClient.parseResults(results)
}
];
this.setState({
items: itemGroups,
items: resourceClient.parseResults(results),
isOpen: true,
loading: false,
error: false
});
})
.catch(error => {
.catch((error: Error) => {
logger.error('MultiSelectAutocomplete error in query', error);
this.setState({

View File

@@ -105,3 +105,11 @@ export interface ISwitchProps {
*/
onChange: (on?: boolean) => void;
}
export type MultiSelectItem = {
content: string;
description?: string;
elemBefore?: Element;
isDisabled?: boolean;
value: string;
};

View File

@@ -20,6 +20,8 @@ interface IProps extends IInputProps {
maxRows?: number;
minRows?: number;
name?: string;
onBlur?: (e: any) => void;
onFocus?: (event: React.FocusEvent) => void;
onKeyPress?: (e: React.KeyboardEvent) => void;
readOnly?: boolean;
required?: boolean;
@@ -148,7 +150,9 @@ const Input = React.forwardRef<any, IProps>(({
maxRows,
minRows,
name,
onBlur,
onChange,
onFocus,
onKeyPress,
placeholder,
readOnly = false,
@@ -208,7 +212,9 @@ const Input = React.forwardRef<any, IProps>(({
{ ...(id ? { id } : {}) }
maxLength = { maxLength }
name = { name }
onBlur = { onBlur }
onChange = { handleChange }
onFocus = { onFocus }
onKeyPress = { onKeyPress }
placeholder = { placeholder }
readOnly = { readOnly }

View File

@@ -0,0 +1,175 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { makeStyles } from 'tss-react/mui';
import { IconCloseLarge } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { MultiSelectItem } from '../types';
import ClickableIcon from './ClickableIcon';
import Input from './Input';
interface IProps {
autoFocus?: boolean;
disabled?: boolean;
error?: boolean;
errorDialog?: JSX.Element | null;
filterValue?: string;
isOpen?: boolean;
items: MultiSelectItem[];
noMatchesText?: string;
onFilterChange?: (value: string) => void;
onRemoved: (item: any) => void;
onSelected: (item: any) => void;
placeholder?: string;
selectedItems?: MultiSelectItem[];
}
const useStyles = makeStyles()(theme => {
return {
container: {
position: 'relative'
},
items: {
'&.found': {
position: 'absolute',
boxShadow: '0px 5px 10px rgba(0, 0, 0, 0.75)'
},
marginTop: theme.spacing(2),
width: '100%',
backgroundColor: theme.palette.ui01,
border: `1px solid ${theme.palette.ui04}`,
borderRadius: `${Number(theme.shape.borderRadius)}px`,
...withPixelLineHeight(theme.typography.bodyShortRegular),
zIndex: 2,
maxHeight: '400px',
overflowY: 'auto',
padding: '0'
},
listItem: {
boxSizing: 'border-box',
display: 'flex',
padding: `${theme.spacing(2)} ${theme.spacing(3)}`,
alignItems: 'center',
'& .content': {
// 38px because of the icon before the content
inlineSize: 'calc(100% - 38px)',
overflowWrap: 'break-word',
marginLeft: theme.spacing(2),
color: theme.palette.text01,
'&.with-remove': {
// 60px because of the icon before the content and the remove button
inlineSize: 'calc(100% - 60px)',
marginRight: theme.spacing(2),
'&.without-before': {
marginLeft: 0,
inlineSize: 'calc(100% - 38px)'
}
},
'&.without-before': {
marginLeft: 0,
inlineSize: '100%'
}
},
'&.found': {
cursor: 'pointer',
padding: `10px ${theme.spacing(3)}`,
'&:hover': {
backgroundColor: theme.palette.ui02
}
},
'&.disabled': {
cursor: 'not-allowed',
'&:hover': {
backgroundColor: theme.palette.ui01
},
color: theme.palette.text03
}
},
errorMessage: {
position: 'absolute',
marginTop: theme.spacing(2),
width: '100%'
}
};
});
const MultiSelect = ({
autoFocus,
disabled,
error,
errorDialog,
placeholder,
items,
filterValue,
onFilterChange,
isOpen,
noMatchesText,
onSelected,
selectedItems,
onRemoved
}: IProps) => {
const { classes } = useStyles();
const inputRef = useRef();
const selectItem = useCallback(item => () => onSelected(item), [ onSelected ]);
const removeItem = useCallback(item => () => onRemoved(item), [ onRemoved ]);
const foundItems = useMemo(() => (
<div className = { `${classes.items} found` }>
{
items.length > 0
? items.map(item => (
<div
className = { `${classes.listItem} ${item.isDisabled ? 'disabled' : ''} found` }
key = { item.value }
onClick = { item.isDisabled ? undefined : selectItem(item) }>
{item.elemBefore}
<div className = { `content ${item.elemBefore ? '' : 'without-before'}` }>
{item.content}
{item.description && <p>{item.description}</p>}
</div>
</div>
))
: <div>{noMatchesText}</div>
}
</div>
), [ items ]);
const errorMessageDialog = useMemo(() =>
error && <div className = { classes.errorMessage }>
{ errorDialog }
</div>, [ error ]);
return (
<div className = { classes.container }>
<Input
autoFocus = { autoFocus }
disabled = { disabled }
onChange = { onFilterChange }
placeholder = { placeholder }
ref = { inputRef }
value = { filterValue ?? '' } />
{isOpen && foundItems}
{ errorMessageDialog }
{ selectedItems && selectedItems?.length > 0 && (
<div className = { classes.items }>
{ selectedItems.map(item => (
<div
className = { `${classes.listItem} ${item.isDisabled ? 'disabled' : ''}` }
key = { item.value }>
{item.elemBefore}
<div className = { `content with-remove ${item.elemBefore ? '' : 'without-before'}` }>
<p>{item.content}</p>
</div>
<ClickableIcon
accessibilityLabel = { 'multi-select-unselect' }
icon = { IconCloseLarge }
id = 'modal-header-close-button'
onClick = { removeItem(item) } />
</div>
))}
</div>
)}
</div>
);
};
export default MultiSelect;

View File

@@ -3,13 +3,12 @@ import { Component } from 'react';
import { createInviteDialogEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { showNotification } from '../../../notifications/actions';
import { showErrorNotification, showNotification } from '../../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants';
import { INotificationProps } from '../../../notifications/types';
import { invite } from '../../actions';
import { INVITE_TYPES } from '../../constants';
import {
GetInviteResultsOptions,
getInviteResultsForQuery,
getInviteTypeCounts,
isAddPeopleEnabled,
@@ -17,7 +16,7 @@ import {
isSipInviteEnabled
} from '../../functions';
import logger from '../../logger';
import { IInvitee } from '../../types';
import { IInviteSelectItem, IInvitee } from '../../types';
export interface IProps {
@@ -26,6 +25,9 @@ export interface IProps {
*/
_addPeopleEnabled: boolean;
/**
* The app id of the user.
*/
_appId: string;
/**
@@ -48,17 +50,17 @@ export interface IProps {
*/
_dialOutRegionUrl: string;
/**
/**
* The JWT token.
*/
_jwt: string;
/**
/**
* The query types used when searching people.
*/
_peopleSearchQueryTypes: Array<string>;
/**
/**
* The URL pointing to the service allowing for people search.
*/
_peopleSearchUrl: string;
@@ -66,7 +68,7 @@ export interface IProps {
/**
* Whether or not to allow sip invites.
*/
_sipInviteEnabled: boolean;
_sipInviteEnabled: boolean;
/**
* The Redux dispatch function.
@@ -90,14 +92,13 @@ export interface IState {
/**
* The list of invite items.
*/
inviteItems: Array<Object>;
inviteItems: Array<IInviteSelectItem>;
}
/**
* Implements an abstract dialog to invite people to the conference.
*/
export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
extends Component<P, S> {
export default class AbstractAddPeopleDialog<P extends IProps, S extends IState> extends Component<P, S> {
/**
* Constructor of the component.
*
@@ -112,7 +113,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
/**
* Retrieves the notification display name for the invitee.
*
* @param {Object} invitee - The invitee object.
* @param {IInvitee} invitee - The invitee object.
* @returns {string}
*/
_getDisplayName(invitee: IInvitee) {
@@ -124,7 +125,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
return invitee.address;
}
return invitee.name;
return invitee.name ?? '';
}
/**
@@ -135,8 +136,8 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
* no invites left to send. If any are left, that means an invite failed
* and an error state should display.
*
* @param {Array<Object>} invitees - The items to be invited.
* @returns {Promise<Array<Object>>}
* @param {Array<IInvitee>} invitees - The items to be invited.
* @returns {Promise<Array<any>>}
*/
_invite(invitees: IInvitee[]) {
const inviteTypeCounts = getInviteTypeCounts(invitees);
@@ -176,10 +177,9 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
'error', 'invite', {
...erroredInviteTypeCounts
}));
this.setState({
addToCallError: true
});
dispatch(showErrorNotification({
titleKey: 'addPeople.failedToAdd'
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
} else if (!_callFlowsEnabled) {
const invitedCount = invitees.length;
let notificationProps: INotificationProps | undefined;
@@ -188,7 +188,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
notificationProps = {
titleArguments: {
name: this._getDisplayName(invitees[0]),
count: invitedCount - 1
count: `${invitedCount - 1}`
},
titleKey: 'notify.invitedThreePlusMembers'
};
@@ -250,7 +250,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
_peopleSearchUrl: peopleSearchUrl,
_sipInviteEnabled: sipInviteEnabled
} = this.props;
const options: GetInviteResultsOptions = {
const options = {
addPeopleEnabled,
appId,
dialOutAuthUrl,
@@ -292,14 +292,14 @@ export function _mapStateToProps(state: IReduxState) {
return {
_addPeopleEnabled: isAddPeopleEnabled(state),
_appId: state['features/base/jwt']?.tenant,
_callFlowsEnabled: callFlowsEnabled,
_dialOutAuthUrl: dialOutAuthUrl,
_dialOutRegionUrl: dialOutRegionUrl,
_appId: state['features/base/jwt']?.tenant ?? '',
_callFlowsEnabled: callFlowsEnabled ?? false,
_dialOutAuthUrl: dialOutAuthUrl ?? '',
_dialOutRegionUrl: dialOutRegionUrl ?? '',
_dialOutEnabled: isDialOutEnabled(state),
_jwt: state['features/base/jwt'].jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl,
_jwt: state['features/base/jwt'].jwt ?? '',
_peopleSearchQueryTypes: peopleSearchQueryTypes ?? [],
_peopleSearchUrl: peopleSearchUrl ?? '',
_sipInviteEnabled: isSipInviteEnabled(state)
};
}

View File

@@ -27,8 +27,6 @@ import CopyMeetingLinkSection from './CopyMeetingLinkSection';
import DialInLimit from './DialInLimit';
import DialInSection from './DialInSection';
import InviteByEmailSection from './InviteByEmailSection';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import InviteContactsSection from './InviteContactsSection';
import LiveStreamSection from './LiveStreamSection';

View File

@@ -1,63 +1,93 @@
// @flow
import InlineMessage from '@atlaskit/inline-message';
import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import React from 'react';
import { connect } from 'react-redux';
import type { Dispatch } from 'redux';
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 MultiSelectAutocomplete from '../../../../base/react/components/web/MultiSelectAutocomplete';
import Button from '../../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../../base/ui/constants.any';
import { isVpaasMeeting } from '../../../../jaas/functions';
import { hideAddPeopleDialog } from '../../../actions';
import { hideAddPeopleDialog } from '../../../actions.web';
import { INVITE_TYPES } from '../../../constants';
import { IInviteSelectItem, IInvitee } from '../../../types';
import AbstractAddPeopleDialog, {
type Props as AbstractProps,
type State,
IProps as AbstractProps,
IState,
_mapStateToProps as _abstractMapStateToProps
} from '../AbstractAddPeopleDialog';
declare var interfaceConfig: Object;
const styles = (theme: Theme) => {
return {
formWrap: {
marginTop: theme.spacing(2)
},
inviteButtons: {
display: 'flex',
justifyContent: 'end',
marginTop: theme.spacing(2),
'& .invite-button': {
marginLeft: theme.spacing(2)
}
}
};
};
type Props = AbstractProps & {
interface IProps extends AbstractProps {
/**
* The {@link JitsiMeetConference} which will be used to invite "room" participants.
*/
_conference: Object,
_conference?: Object;
/**
* Whether the meeting belongs to JaaS user.
*/
_isVpaas: boolean,
_isVpaas?: boolean;
/**
* Css classes.
*/
classes: any;
/**
* The redux {@code dispatch} function.
*/
dispatch: Dispatch<any>,
dispatch: IStore['dispatch'];
/**
* Invoked to obtain translated strings.
*/
t: Function,
};
t: Function;
}
/**
* Form that enables inviting others to the call.
*/
class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
_multiselect = null;
class InviteContactsForm extends AbstractAddPeopleDialog<IProps, IState> {
_multiselect: MultiSelectAutocomplete | null = null;
_resourceClient: Object;
_resourceClient: {
makeQuery: (query: string) => Promise<Array<any>>;
parseResults: Function;
};
_translations: Object;
_translations: {
[key: string]: string;
_addPeopleEnabled: string;
_dialOutEnabled: string;
_sipInviteEnabled: string;
};
state = {
addToCallError: false,
addToCallInProgress: false,
inviteItems: []
inviteItems: [] as IInviteSelectItem[]
};
/**
@@ -66,7 +96,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
@@ -99,11 +129,11 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
/**
* React Component method that executes once component is updated.
*
* @param {Object} prevProps - The state object before the update.
* @param {Object} prevState - The state object before the update.
* @param {Props} prevProps - The props object before the update.
* @param {State} prevState - The state object before the update.
* @returns {void}
*/
componentDidUpdate(prevProps, prevState) {
componentDidUpdate(prevProps: IProps, prevState: IState) {
/**
* Clears selected items from the multi select component on successful
* invite.
@@ -133,7 +163,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
const loadingMessage = 'addPeople.searching';
const noMatches = 'addPeople.noResults';
const features = {
const features: { [key: string]: boolean; } = {
_dialOutEnabled,
_addPeopleEnabled,
_sipInviteEnabled
@@ -152,9 +182,8 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
return (
<div
className = 'add-people-form-wrap'
className = { this.props.classes.formWrap }
onKeyDown = { this._onKeyDown }>
{ this._renderErrorMessage() }
<MultiSelectAutocomplete
isDisabled = { isMultiSelectDisabled }
loadingMessage = { t(loadingMessage) }
@@ -172,21 +201,17 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
);
}
_invite: Array<Object> => Promise<*>;
_isAddDisabled: () => boolean;
_onItemSelected: (Object) => Object;
/**
* Callback invoked when a selection has been made but before it has been
* set as selected.
*
* @param {Object} item - The item that has just been selected.
* @param {IInviteSelectItem} item - The item that has just been selected.
* @private
* @returns {Object} The item to display as selected in the input.
*/
_onItemSelected(item) {
_onItemSelected(item: IInviteSelectItem) {
if (item.item.type === INVITE_TYPES.PHONE) {
item.content = item.item.number;
}
@@ -194,22 +219,19 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
return item;
}
_onSelectionChange: (Map<*, *>) => void;
/**
* Handles a selection change.
*
* @param {Array} selectedItems - The list of selected items.
* @param {Array<IInviteSelectItem>} selectedItems - The list of selected items.
* @private
* @returns {void}
*/
_onSelectionChange(selectedItems) {
_onSelectionChange(selectedItems: IInviteSelectItem[]) {
this.setState({
inviteItems: selectedItems
});
}
_onSubmit: () => void;
/**
* Submits the selection for inviting.
@@ -222,49 +244,43 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
const invitees = inviteItems.map(({ item }) => item);
this._invite(invitees)
.then(invitesLeftToSend => {
.then((invitesLeftToSend: IInvitee[]) => {
if (invitesLeftToSend.length) {
const unsentInviteIDs
= invitesLeftToSend.map(invitee =>
invitee.id || invitee.user_id || invitee.number);
const itemsToSelect
= inviteItems.filter(({ item }) =>
unsentInviteIDs.includes(item.id || item.user_id || item.number));
const itemsToSelect = inviteItems.filter(({ item }) =>
unsentInviteIDs.includes(item.id || item.user_id || item.number));
if (this._multiselect) {
this._multiselect.setSelectedItems(itemsToSelect);
}
} else {
this.props.dispatch(hideAddPeopleDialog());
}
});
})
.finally(() => this.props.dispatch(hideAddPeopleDialog()));
}
_onSubmitKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
* @param {KeyboardEvent} e - The key event to handle.
*
* @returns {void}
*/
_onSubmitKeyPress(e) {
_onSubmitKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onSubmit();
}
}
_onKeyDown: (Object) => void;
/**
* Handles 'Enter' key in the form to trigger the invite.
*
* @param {Object} event - The key event.
* @param {KeyboardEvent} event - The key event.
* @returns {void}
*/
_onKeyDown(event) {
_onKeyDown(event: React.KeyboardEvent) {
const { inviteItems } = this.state;
if (event.key === 'Enter') {
@@ -275,20 +291,19 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
}
}
_parseQueryResults: (?Array<Object>) => Array<Object>;
/**
* Returns the avatar component for a user.
*
* @param {Object} user - The user.
* @param {any} user - The user.
* @param {string} className - The CSS class for the avatar component.
* @private
* @returns {ReactElement}
*/
_getAvatar(user, className = 'avatar-small') {
_getAvatar(user: any, className = 'avatar-small') {
return (
<Avatar
className = { className }
size = { 32 }
status = { user.status }
url = { user.avatar } />
);
@@ -305,7 +320,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
* @returns {Object[]} Configuration objects for items to display in the
* search autocomplete.
*/
_parseQueryResults(response = []) {
_parseQueryResults(response: IInvitee[] = []) {
const { t, _dialOutEnabled } = this.props;
const userTypes = [ INVITE_TYPES.USER, INVITE_TYPES.VIDEO_ROOM, INVITE_TYPES.ROOM ];
@@ -394,10 +409,6 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
];
}
_query: (string) => Promise<Array<Object>>;
_onClearItems: () => void;
/**
* Clears the selected items from state and form.
*
@@ -410,16 +421,14 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
this.setState({ inviteItems: [] });
}
_onClearItemsKeyPress: () => void;
/**
* Clears the selected items from state and form.
*
* @param {Object} e - The key event to handle.
* @param {KeyboardEvent} e - The key event to handle.
*
* @returns {void}
*/
_onClearItemsKeyPress(e) {
_onClearItemsKeyPress(e: KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onClearItems();
@@ -433,76 +442,30 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
*/
_renderFormActions() {
const { inviteItems } = this.state;
const { t } = this.props;
const { t, classes } = this.props;
if (!inviteItems.length) {
return null;
}
return (
<div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }>
<a
<div className = { classes.inviteButtons }>
<Button
aria-label = { t('dialog.Cancel') }
className = 'invite-more-dialog invite-buttons-cancel'
className = 'invite-button'
label = { t('dialog.Cancel') }
onClick = { this._onClearItems }
onKeyPress = { this._onClearItemsKeyPress }
role = 'button'
tabIndex = { 0 }>
{t('dialog.Cancel')}
</a>
<a
type = { BUTTON_TYPES.SECONDARY } />
<Button
aria-label = { t('addPeople.add') }
className = 'invite-more-dialog invite-buttons-add'
className = 'invite-button'
disabled = { this._isAddDisabled() }
label = { t('addPeople.add') }
onClick = { this._onSubmit }
onKeyPress = { this._onSubmitKeyPress }
role = 'button'
tabIndex = { 0 }>
{t('addPeople.add')}
</a>
</div>
);
}
/**
* Renders the error message if the add doesn't succeed.
*
* @private
* @returns {ReactElement|null}
*/
_renderErrorMessage() {
if (!this.state.addToCallError) {
return null;
}
const { t } = this.props;
const supportString = t('inlineDialogFailure.supportMsg');
const supportLink = interfaceConfig.SUPPORT_URL;
const supportLinkContent = supportLink ? (
<span>
<span>
{ supportString.padEnd(supportString.length + 1) }
</span>
<span>
<a
aria-label = { supportLink }
href = { supportLink }
rel = 'noopener noreferrer'
target = '_blank'>
{ t('inlineDialogFailure.support') }
</a>
</span>
<span>.</span>
</span>
) : null;
return (
<div className = 'modal-dialog-form-error'>
<InlineMessage
title = { t('addPeople.failedToAdd') }
type = 'error'>
{ supportLinkContent }
</InlineMessage>
role = 'button' />
</div>
);
}
@@ -515,23 +478,19 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
*/
_renderTelephoneIcon() {
return (
<span className = 'add-telephone-icon'>
<Icon src = { IconPhoneRinging } />
</span>
<Icon src = { IconPhoneRinging } />
);
}
_setMultiSelectElement: (React$ElementRef<*> | null) => void;
/**
* Sets the instance variable for the multi select component
* element so it can be accessed directly.
*
* @param {Object} element - The DOM element for the component's dialog.
* @param {MultiSelectAutocomplete} element - The DOM element for the component's dialog.
* @private
* @returns {void}
*/
_setMultiSelectElement(element) {
_setMultiSelectElement(element: MultiSelectAutocomplete) {
this._multiselect = element;
}
}
@@ -540,15 +499,15 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
* Maps (parts of) the Redux state to the associated
* {@code AddPeopleDialog}'s props.
*
* @param {Object} state - The Redux state.
* @param {IReduxState} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
function _mapStateToProps(state: IReduxState) {
return {
..._abstractMapStateToProps(state),
_isVpaas: isVpaasMeeting(state)
};
}
export default translate(connect(_mapStateToProps)(InviteContactsForm));
export default translate(connect(_mapStateToProps)(withStyles(styles)(InviteContactsForm)));

View File

@@ -1,25 +1,16 @@
// @flow
import React from 'react';
import { translate } from '../../../../base/i18n/functions';
import { useTranslation } from 'react-i18next';
import InviteContactsForm from './InviteContactsForm';
type Props = {
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* Component that represents the invitation section of the {@code AddPeopleDialog}.
*
* @returns {ReactElement$<any>}
*/
function InviteContactsSection({ t }: Props) {
function InviteContactsSection() {
const { t } = useTranslation();
return (
<>
<span>{t('addPeople.addContacts')}</span>
@@ -29,4 +20,4 @@ function InviteContactsSection({ t }: Props) {
);
}
export default translate(InviteContactsSection);
export default InviteContactsSection;

View File

@@ -1,6 +1,20 @@
import { MultiSelectItem } from '../base/ui/components/types';
export interface IInvitee {
address: string;
name: string;
allowed?: boolean;
id?: string;
name?: string;
number: string;
originalEntry?: string;
phone?: string;
showCountryCodeReminder?: boolean;
type: string;
user_id?: string;
}
export interface IInviteSelectItem extends MultiSelectItem {
filterValues?: string[];
item: IInvitee;
tag?: any;
}

View File

@@ -40,4 +40,4 @@ export const NOTIFY_CLICK_MODE = {
};
// Around 300 to be displayed above components like chat
export const ZINDEX_DIALOG_PORTAL = 300;
export const ZINDEX_DIALOG_PORTAL = 302;