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';
@import 'filmstrip/vertical_filmstrip_overrides'; @import 'filmstrip/vertical_filmstrip_overrides';
@import 'unsupported-browser/main'; @import 'unsupported-browser/main';
@import 'modals/invite/add-people';
@import 'deep-linking/main'; @import 'deep-linking/main';
@import 'transcription-subtitles'; @import 'transcription-subtitles';
@import '_meetings_list.scss'; @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. * 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 { .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 { &-dialog {
color: #fff; color: #fff;
font-size: 15px; font-size: 15px;
@@ -65,59 +12,6 @@
background: #5E6D7A; 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 { &.stream {
display: flex; display: flex;
justify-content: space-between; 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 React from 'react';
import { WithTranslation } from 'react-i18next'; 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'; 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}. * 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. * 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. * Inline dialog that represents a failure and allows a retry.
*
* @returns {Element}
*/ */
class InlineDialogFailure extends Component<IProps> { const InlineDialogFailure = ({
/** onRetry,
* Renders the content of this component. showSupportLink
* }: IProps) => {
* @returns {ReactElement} const { t } = useTranslation();
*/ const { classes } = useStyles();
render() {
const { t, showSupportLink } = this.props;
const supportLink = interfaceConfig.SUPPORT_URL; const supportLink = interfaceConfig.SUPPORT_URL;
const supportString = t('inlineDialogFailure.supportMsg'); const supportString = t('inlineDialogFailure.supportMsg');
const supportLinkElem const supportLinkElem = supportLink && showSupportLink
= supportLink && showSupportLink ? (
? ( <div>
<div className = 'inline-dialog-error-text'> <span>{ supportString.padEnd(supportString.length + 1) }
<span>{ supportString.padEnd(supportString.length + 1) } </span>
</span> <span>
<span> <a
<a href = { supportLink }
href = { supportLink } rel = 'noopener noreferrer'
rel = 'noopener noreferrer' target = '_blank'>
target = '_blank'> { t('inlineDialogFailure.support') }
{ t('inlineDialogFailure.support') } </a>
</a> </span>
</span> <span>.</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 } />
</div> </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 _debounce from 'lodash/debounce';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { MultiSelectItem } from '../../../ui/components/types';
import MultiSelect from '../../../ui/components/web/MultiSelect';
import logger from '../../logger'; import logger from '../../logger';
import InlineDialogFailure from './InlineDialogFailure'; import InlineDialogFailure from './InlineDialogFailure';
@@ -13,119 +11,119 @@ import InlineDialogFailure from './InlineDialogFailure';
* The type of the React {@code Component} props of * The type of the React {@code Component} props of
* {@link MultiSelectAutocomplete}. * {@link MultiSelectAutocomplete}.
*/ */
type Props = { interface IProps {
/** /**
* The default value of the selected item. * The default value of the selected item.
*/ */
defaultValue: Array<Object>, defaultValue?: Array<any>;
/** /**
* Optional footer to show as a last element in the results. * Optional footer to show as a last element in the results.
* Should be of type {content: <some content>}. * Should be of type {content: <some content>}.
*/ */
footer: Object, footer?: any;
/** /**
* Indicates if the component is disabled. * Indicates if the component is disabled.
*/ */
isDisabled: boolean, isDisabled: boolean;
/** /**
* Text to display while a query is executing. * Text to display while a query is executing.
*/ */
loadingMessage: string, loadingMessage: string;
/** /**
* The text to show when no matches are found. * The text to show when no matches are found.
*/ */
noMatchesFound: string, noMatchesFound: string;
/** /**
* The function called immediately before a selection has been actually * The function called immediately before a selection has been actually
* selected. Provides an opportunity to do any formatting. * selected. Provides an opportunity to do any formatting.
*/ */
onItemSelected: Function, onItemSelected: Function;
/** /**
* The function called when the selection changes. * The function called when the selection changes.
*/ */
onSelectionChange: Function, onSelectionChange: Function;
/** /**
* The placeholder text of the input component. * The placeholder text of the input component.
*/ */
placeholder: string, placeholder: string;
/** /**
* The service providing the search. * The service providing the search.
*/ */
resourceClient: { makeQuery: Function, parseResults: Function }, resourceClient: { makeQuery: Function; parseResults: Function; };
/** /**
* Indicates if the component should fit the container. * Indicates if the component should fit the container.
*/ */
shouldFitContainer: boolean, shouldFitContainer: boolean;
/** /**
* Indicates if we should focus. * Indicates if we should focus.
*/ */
shouldFocus: boolean, shouldFocus: boolean;
/** /**
* Indicates whether the support link should be shown in case of an error. * 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 * The type of the React {@code Component} state of
* {@link MultiSelectAutocomplete}. * {@link MultiSelectAutocomplete}.
*/ */
type State = { interface IState {
/**
* 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,
/** /**
* Indicates if there was an error. * 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. * 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. * The list of selected items.
*/ */
selectedItems: Array<Object> selectedItems: Array<MultiSelectItem>;
}; }
/** /**
* A MultiSelect that is also auto-completing. * 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. * Initializes a new {@code MultiSelectAutocomplete} instance.
* *
* @param {Object} props - The read-only properties with which the new * @param {Object} props - The read-only properties with which the new
* instance is to be initialized. * instance is to be initialized.
*/ */
constructor(props: Props) { constructor(props: IProps) {
super(props); super(props);
const defaultValue = this.props.defaultValue || []; const defaultValue = this.props.defaultValue || [];
@@ -148,11 +146,11 @@ class MultiSelectAutocomplete extends Component<Props, State> {
/** /**
* Sets the items to display as selected. * 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. * having been selected.
* @returns {void} * @returns {void}
*/ */
setSelectedItems(selectedItems: Array<Object> = []) { setSelectedItems(selectedItems: Array<MultiSelectItem> = []) {
this.setState({ selectedItems }); this.setState({ selectedItems });
} }
@@ -162,37 +160,32 @@ class MultiSelectAutocomplete extends Component<Props, State> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const shouldFitContainer = this.props.shouldFitContainer || false; const autoFocus = this.props.shouldFocus || false;
const shouldFocus = this.props.shouldFocus || false; const disabled = this.props.isDisabled || false;
const isDisabled = this.props.isDisabled || false;
const placeholder = this.props.placeholder || ''; const placeholder = this.props.placeholder || '';
const noMatchesFound = this.props.noMatchesFound || ''; const noMatchesFound = this.props.noMatchesFound || '';
const errorDialog = this._renderError();
return ( return (
<div> <div>
<MultiSelectStateless <MultiSelect
autoFocus = { autoFocus }
disabled = { disabled }
error = { this.state.error }
errorDialog = { errorDialog }
filterValue = { this.state.filterValue } filterValue = { this.state.filterValue }
footer = { this.props.footer }
icon = { null }
isDisabled = { isDisabled }
isLoading = { this.state.loading }
isOpen = { this.state.isOpen } isOpen = { this.state.isOpen }
items = { this.state.items } items = { this.state.items }
loadingMessage = { this.props.loadingMessage } noMatchesText = { noMatchesFound }
noMatchesFound = { noMatchesFound }
onFilterChange = { this._onFilterChange } onFilterChange = { this._onFilterChange }
onRemoved = { this._onSelectionChange } onRemoved = { this._onSelectionChange }
onSelected = { this._onSelectionChange } onSelected = { this._onSelectionChange }
placeholder = { placeholder } placeholder = { placeholder }
selectedItems = { this.state.selectedItems } selectedItems = { this.state.selectedItems } />
shouldFitContainer = { shouldFitContainer }
shouldFocus = { shouldFocus } />
{ this._renderError() }
</div> </div>
); );
} }
_onFilterChange: (string) => void;
/** /**
* Sets the state and sends a query on filter change. * Sets the state and sends a query on filter change.
@@ -201,7 +194,7 @@ class MultiSelectAutocomplete extends Component<Props, State> {
* @private * @private
* @returns {void} * @returns {void}
*/ */
_onFilterChange(filterValue) { _onFilterChange(filterValue: string) {
this.setState({ this.setState({
// Clean the error if the filterValue is empty. // Clean the error if the filterValue is empty.
error: this.state.error && Boolean(filterValue), error: this.state.error && Boolean(filterValue),
@@ -215,8 +208,6 @@ class MultiSelectAutocomplete extends Component<Props, State> {
} }
} }
_onRetry: () => void;
/** /**
* Retries the query on retry. * Retries the query on retry.
* *
@@ -227,18 +218,16 @@ class MultiSelectAutocomplete extends Component<Props, State> {
this._sendQuery(this.state.filterValue); this._sendQuery(this.state.filterValue);
} }
_onSelectionChange: (Object) => void;
/** /**
* Updates the selected items when a selection event occurs. * Updates the selected items when a selection event occurs.
* *
* @param {Object} item - The selected item. * @param {any} item - The selected item.
* @private * @private
* @returns {void} * @returns {void}
*/ */
_onSelectionChange(item) { _onSelectionChange(item: any) {
const existing 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; let selectedItems = this.state.selectedItems;
if (existing) { if (existing) {
@@ -265,30 +254,22 @@ class MultiSelectAutocomplete extends Component<Props, State> {
if (!this.state.error) { if (!this.state.error) {
return null; return null;
} }
const content = (
<div className = 'autocomplete-error'>
<InlineDialogFailure
onRetry = { this._onRetry }
showSupportLink = { this.props.showSupportLink } />
</div>
);
return ( return (
<AKInlineDialog
content = { content } <InlineDialogFailure
isOpen = { true } /> onRetry = { this._onRetry }
showSupportLink = { this.props.showSupportLink } />
); );
} }
_sendQuery: (string) => void;
/** /**
* Sends a query to the resourceClient. * Sends a query to the resourceClient.
* *
* @param {string} filterValue - The string to use for the search. * @param {string} filterValue - The string to use for the search.
* @returns {void} * @returns {void}
*/ */
_sendQuery(filterValue) { _sendQuery(filterValue: string) {
if (!filterValue) { if (!filterValue) {
return; return;
} }
@@ -299,11 +280,11 @@ class MultiSelectAutocomplete extends Component<Props, State> {
const resourceClient = this.props.resourceClient || { const resourceClient = this.props.resourceClient || {
makeQuery: () => Promise.resolve([]), makeQuery: () => Promise.resolve([]),
parseResults: results => results parseResults: (results: any) => results
}; };
resourceClient.makeQuery(filterValue) resourceClient.makeQuery(filterValue)
.then(results => { .then((results: any) => {
if (this.state.filterValue !== filterValue) { if (this.state.filterValue !== filterValue) {
this.setState({ this.setState({
error: false error: false
@@ -311,20 +292,15 @@ class MultiSelectAutocomplete extends Component<Props, State> {
return; return;
} }
const itemGroups = [
{
items: resourceClient.parseResults(results)
}
];
this.setState({ this.setState({
items: itemGroups, items: resourceClient.parseResults(results),
isOpen: true, isOpen: true,
loading: false, loading: false,
error: false error: false
}); });
}) })
.catch(error => { .catch((error: Error) => {
logger.error('MultiSelectAutocomplete error in query', error); logger.error('MultiSelectAutocomplete error in query', error);
this.setState({ this.setState({

View File

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

View File

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

View File

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