import _ from 'lodash'; import React, { ReactElement } from 'react'; import { ActivityIndicator, FlatList, TouchableOpacity, View } from 'react-native'; import { AlertDialog, openDialog } from '../../../../base/dialog'; import { translate } from '../../../../base/i18n'; import { Icon, IconCheck, IconCloseCircle, IconPhoneRinging, IconSearch, IconShare } from '../../../../base/icons'; import JitsiScreen from '../../../../base/modal/components/JitsiScreen'; import { AvatarListItem, type Item } from '../../../../base/react'; import { connect } from '../../../../base/redux'; import BaseTheme from '../../../../base/ui/components/BaseTheme.native'; import Input from '../../../../base/ui/components/native/Input'; import HeaderNavigationButton from '../../../../mobile/navigation/components/HeaderNavigationButton'; import { beginShareRoom } from '../../../../share-room'; import { INVITE_TYPES } from '../../../constants'; import AbstractAddPeopleDialog, { type Props as AbstractProps, type State as AbstractState, _mapStateToProps as _abstractMapStateToProps } from '../AbstractAddPeopleDialog'; import styles, { AVATAR_SIZE } from './styles'; type Props = AbstractProps & { /** * True if the invite dialog should be open, false otherwise. */ _isVisible: boolean, /** * Default prop for navigation between screen components(React Navigation). */ navigation: Object, /** * Function used to translate i18n labels. */ t: Function, /** * Theme used for styles. */ theme: Object }; type State = AbstractState & { /** * Boolean to show if an extra padding needs to be added to the bottom bar. */ bottomPadding: boolean, /** * State variable to keep track of the search field value. */ fieldValue: string, /** * True if a search is in progress, false otherwise. */ searchInprogress: boolean, /** * An array of items that are selectable on this dialog. This is usually * populated by an async search. */ selectableItems: Array }; /** * Implements a special dialog to invite people from a directory service. */ class AddPeopleDialog extends AbstractAddPeopleDialog { /** * Default state object to reset the state to when needed. */ defaultState = { addToCallError: false, addToCallInProgress: false, bottomPadding: false, fieldValue: '', inviteItems: [], searchInprogress: false, selectableItems: [] }; /** * TimeoutID to delay the search for the time the user is probably typing. */ /* eslint-disable-next-line no-undef */ searchTimeout: TimeoutID; /** * Contrustor of the component. * * @inheritdoc */ constructor(props: Props) { super(props); this.state = this.defaultState; this._keyExtractor = this._keyExtractor.bind(this); this._renderInvitedItem = this._renderInvitedItem.bind(this); this._renderItem = this._renderItem.bind(this); this._renderSeparator = this._renderSeparator.bind(this); this._onClearField = this._onClearField.bind(this); this._onInvite = this._onInvite.bind(this); this._onPressItem = this._onPressItem.bind(this); this._onShareMeeting = this._onShareMeeting.bind(this); this._onTypeQuery = this._onTypeQuery.bind(this); this._renderShareMeetingButton = this._renderShareMeetingButton.bind(this); this._renderIcon = this._renderIcon.bind(this); } /** * Implements React's {@link Component#componentDidMount()}. Invoked * immediately after this component is mounted. * * @inheritdoc * @returns {void} */ componentDidMount() { const { navigation, t } = this.props; navigation.setOptions({ headerRight: () => ( ) }); } /** * Implements {@code Component#componentDidUpdate}. * * @inheritdoc */ componentDidUpdate(prevProps) { const { navigation, t } = this.props; navigation.setOptions({ // eslint-disable-next-line react/no-multi-comp headerRight: () => ( ) }); if (prevProps._isVisible !== this.props._isVisible) { // Clear state this._clearState(); } } /** * Implements {@code Component#render}. * * @inheritdoc */ render() { const { _addPeopleEnabled, _dialOutEnabled } = this.props; const { inviteItems, selectableItems } = this.state; let placeholderKey = 'searchPlaceholder'; if (!_addPeopleEnabled) { placeholderKey = 'searchCallOnlyPlaceholder'; } else if (!_dialOutEnabled) { placeholderKey = 'searchPeopleOnlyPlaceholder'; } return ( { Boolean(inviteItems.length) && } ); } /** * Clears the dialog content. * * @returns {void} */ _clearState() { this.setState(this.defaultState); } /** * Returns an object capable of being rendered by an {@code AvatarListItem}. * * @param {Object} flatListItem - An item of the data array of the {@code FlatList}. * @returns {?Object} */ _getRenderableItem(flatListItem) { const { item } = flatListItem; switch (item.type) { case INVITE_TYPES.PHONE: return { avatar: IconPhoneRinging, key: item.number, title: item.number }; case INVITE_TYPES.USER: return { avatar: item.avatar, key: item.id || item.user_id, title: item.name }; default: return null; } } _invite: Array => Promise>; _isAddDisabled: () => boolean; _keyExtractor: Object => string; /** * Key extractor for the flatlist. * * @param {Object} item - The flatlist item that we need the key to be * generated for. * @returns {string} */ _keyExtractor(item) { return item.type === INVITE_TYPES.USER ? item.id || item.user_id : item.number; } _onClearField: () => void; /** * Callback to clear the text field. * * @returns {void} */ _onClearField() { this.setState({ fieldValue: '' }); // Clear search results this._onTypeQuery(''); } _onInvite: () => void; /** * Invites the selected entries. * * @returns {void} */ _onInvite() { this._invite(this.state.inviteItems) .then(invitesLeftToSend => { if (invitesLeftToSend.length) { this.setState({ inviteItems: invitesLeftToSend }); this._showFailedInviteAlert(); } }); } _onPressItem: Item => Function; /** * Function to prepare a callback for the onPress event of the touchable. * * @param {Item} item - The item on which onPress was invoked. * @returns {Function} */ _onPressItem(item) { return () => { const { inviteItems } = this.state; const finderKey = item.type === INVITE_TYPES.PHONE ? 'number' : 'user_id'; if (inviteItems.find( _.matchesProperty(finderKey, item[finderKey]))) { // Item is already selected, need to unselect it. this.setState({ inviteItems: inviteItems.filter( element => item[finderKey] !== element[finderKey]) }); } else { // Item is not selected yet, need to add to the list. const items: Array = inviteItems.concat(item); this.setState({ inviteItems: _.sortBy(items, [ 'name', 'number' ]) }); } }; } _onShareMeeting: () => void; /** * Shows the system share sheet to share the meeting information. * * @returns {void} */ _onShareMeeting() { if (this.state.inviteItems.length > 0) { // The use probably intended to invite people. this._onInvite(); } else { this.props.dispatch(beginShareRoom()); } } _onTypeQuery: string => void; /** * Handles the typing event of the text field on the dialog and performs the * search. * * @param {string} query - The query that is typed in the field. * @returns {void} */ _onTypeQuery(query) { this.setState({ fieldValue: query }); clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(() => { this.setState({ searchInprogress: true }, () => { this._performSearch(query); }); }, 500); } /** * Performs the actual search. * * @param {string} query - The query to search for. * @returns {void} */ _performSearch(query) { this._query(query).then(results => { this.setState({ selectableItems: _.sortBy(results, [ 'name', 'number' ]) }); }) .finally(() => { this.setState({ searchInprogress: false }); }); } _query: (string) => Promise>; _renderInvitedItem: Object => ReactElement | null; /** * Renders a single item in the invited {@code FlatList}. * * @param {Object} flatListItem - An item of the data array of the * {@code FlatList}. * @param {number} index - The index of the currently rendered item. * @returns {?React$Element} */ _renderInvitedItem(flatListItem, index): ReactElement | null { const { item } = flatListItem; const renderableItem = this._getRenderableItem(flatListItem); return ( ); } _renderItem: Object => ReactElement | null; /** * Renders a single item in the search result {@code FlatList}. * * @param {Object} flatListItem - An item of the data array of the * {@code FlatList}. * @param {number} index - The index of the currently rendered item. * @returns {?React$Element<*>} */ _renderItem(flatListItem, index): ReactElement | null { const { item } = flatListItem; const { inviteItems } = this.state; let selected = false; const renderableItem = this._getRenderableItem(flatListItem); if (!renderableItem) { return null; } switch (item.type) { case INVITE_TYPES.PHONE: selected = inviteItems.find(_.matchesProperty('number', item.number)); break; case INVITE_TYPES.USER: selected = item.id ? inviteItems.find(_.matchesProperty('id', item.id)) : inviteItems.find(_.matchesProperty('user_id', item.user_id)); break; default: return null; } return ( { selected && } ); } _renderSeparator: () => ReactElement | null; /** * Renders the item separator. * * @returns {?React$Element<*>} */ _renderSeparator() { return ( ); } _renderShareMeetingButton: () => ReactElement; /** * Renders a button to share the meeting info. * * @returns {React#Element<*>} */ _renderShareMeetingButton() { return ( ); } _renderIcon: () => ReactElement; /** * Renders an icon. * * @returns {React#Element<*>} */ _renderIcon() { if (this.state.searchInprogress) { return ( ); } return ( ); } /** * Shows an alert telling the user that some invitees were failed to be * invited. * * NOTE: We're using an Alert here because we're on a modal and it makes * using our dialogs a tad more difficult. * * @returns {void} */ _showFailedInviteAlert() { this.props.dispatch(openDialog(AlertDialog, { contentKey: { key: 'inviteDialog.alertText' } })); } } /** * Maps part of the Redux state to the props of this component. * * @param {Object} state - The Redux state. * @returns {{ * _isVisible: boolean * }} */ function _mapStateToProps(state: Object) { return { ..._abstractMapStateToProps(state) }; } export default translate(connect(_mapStateToProps)(AddPeopleDialog));