123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627 |
- // @flow
-
- import _ from 'lodash';
- import React from 'react';
- import {
- ActivityIndicator,
- FlatList,
- Platform,
- SafeAreaView,
- TextInput,
- TouchableOpacity,
- View
- } from 'react-native';
-
- import { ColorSchemeRegistry } from '../../../../base/color-scheme';
- import { AlertDialog, openDialog } from '../../../../base/dialog';
- import { translate } from '../../../../base/i18n';
- import {
- Icon,
- IconCancelSelection,
- IconCheck,
- IconClose,
- IconPhone,
- IconSearch,
- IconShare
- } from '../../../../base/icons';
- import { JitsiModal, setActiveModalId } from '../../../../base/modal';
- import {
- AvatarListItem,
- type Item
- } from '../../../../base/react';
- import { connect } from '../../../../base/redux';
- import { beginShareRoom } from '../../../../share-room';
- import { ADD_PEOPLE_DIALOG_VIEW_ID, INVITE_TYPES } from '../../../constants';
- import AbstractAddPeopleDialog, {
- type Props as AbstractProps,
- type State as AbstractState,
- _mapStateToProps as _abstractMapStateToProps
- } from '../AbstractAddPeopleDialog';
-
- import styles, {
- AVATAR_SIZE,
- DARK_GREY
- } from './styles';
-
- type Props = AbstractProps & {
-
- /**
- * The color schemed style of the Header.
- */
- _headerStyles: Object,
-
- /**
- * True if the invite dialog should be open, false otherwise.
- */
- _isVisible: boolean,
-
- /**
- * Function used to translate i18n labels.
- */
- t: Function
- };
-
- 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<Object>
- };
-
- /**
- * Implements a special dialog to invite people from a directory service.
- */
- class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
- /**
- * Default state object to reset the state to when needed.
- */
- defaultState = {
- addToCallError: false,
- addToCallInProgress: false,
- bottomPadding: false,
- fieldValue: '',
- inviteItems: [],
- searchInprogress: false,
- selectableItems: []
- };
-
- /**
- * Ref of the search field.
- */
- inputFieldRef: ?TextInput;
-
- /**
- * TimeoutID to delay the search for the time the user is probably typing.
- */
- 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._setFieldRef = this._setFieldRef.bind(this);
- }
-
- /**
- * Implements {@code Component#componentDidUpdate}.
- *
- * @inheritdoc
- */
- componentDidUpdate(prevProps) {
- 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 (
- <JitsiModal
- footerComponent = { this._renderShareMeetingButton }
- headerProps = {{
- forwardDisabled: this._isAddDisabled(),
- forwardLabelKey: 'inviteDialog.send',
- headerLabelKey: 'inviteDialog.header',
- onPressForward: this._onInvite
- }}
- modalId = { ADD_PEOPLE_DIALOG_VIEW_ID }>
- <View
- style = { styles.searchFieldWrapper }>
- <View style = { styles.searchIconWrapper }>
- { this.state.searchInprogress
- ? <ActivityIndicator
- color = { DARK_GREY }
- size = 'small' />
- : <Icon
- src = { IconSearch }
- style = { styles.searchIcon } />}
- </View>
- <TextInput
- autoCorrect = { false }
- autoFocus = { false }
- onBlur = { this._onFocused(false) }
- onChangeText = { this._onTypeQuery }
- onFocus = { this._onFocused(true) }
- placeholder = {
- this.props.t(`inviteDialog.${placeholderKey}`)
- }
- ref = { this._setFieldRef }
- style = { styles.searchField }
- value = { this.state.fieldValue } />
- { this._renderClearButton() }
- </View>
- { Boolean(inviteItems.length) && <View style = { styles.invitedList }>
- <FlatList
- data = { inviteItems }
- horizontal = { true }
- keyExtractor = { this._keyExtractor }
- keyboardShouldPersistTaps = 'always'
- renderItem = { this._renderInvitedItem } />
- </View> }
- <View style = { styles.resultList }>
- <FlatList
- ItemSeparatorComponent = { this._renderSeparator }
- data = { selectableItems }
- extraData = { inviteItems }
- keyExtractor = { this._keyExtractor }
- keyboardShouldPersistTaps = 'always'
- renderItem = { this._renderItem } />
- </View>
- </JitsiModal>
- );
- }
-
- /**
- * 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: IconPhone,
- 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<Object> => Promise<Array<Object>>
-
- _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('');
- }
-
- _onFocused: boolean => Function;
-
- /**
- * Constructs a callback to be used to update the padding of the field if necessary.
- *
- * @param {boolean} focused - True of the field is focused.
- * @returns {Function}
- */
- _onFocused(focused) {
- return () => {
- Platform.OS === 'android' && this.setState({
- bottomPadding: focused
- });
- };
- }
-
- _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();
- } else {
- this.props.dispatch(setActiveModalId());
- }
- });
- }
-
- _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<Object> = 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
- }, () => {
- this.inputFieldRef && this.inputFieldRef.focus();
- });
- });
- }
-
- _query: (string) => Promise<Array<Object>>;
-
- /**
- * Renders a button to clear the text field.
- *
- * @returns {React#Element<*>}
- */
- _renderClearButton() {
- if (!this.state.fieldValue.length) {
- return null;
- }
-
- return (
- <TouchableOpacity
- onPress = { this._onClearField }
- style = { styles.clearButton }>
- <View style = { styles.clearIconContainer }>
- <Icon
- src = { IconClose }
- style = { styles.clearIcon } />
- </View>
- </TouchableOpacity>
- );
- }
-
- _renderInvitedItem: Object => React$Element<any> | 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<any>}
- */
- _renderInvitedItem(flatListItem, index): React$Element<any> | null {
- const { item } = flatListItem;
- const renderableItem = this._getRenderableItem(flatListItem);
-
- return (
- <TouchableOpacity onPress = { this._onPressItem(item) } >
- <View
- pointerEvents = 'box-only'
- style = { styles.itemWrapper }>
- <AvatarListItem
- avatarOnly = { true }
- avatarSize = { AVATAR_SIZE }
- avatarStatus = { item.status }
- avatarStyle = { styles.avatar }
- avatarTextStyle = { styles.avatarText }
- item = { renderableItem }
- key = { index }
- linesStyle = { styles.itemLinesStyle }
- titleStyle = { styles.itemText } />
- <Icon
- src = { IconCancelSelection }
- style = { styles.unselectIcon } />
- </View>
- </TouchableOpacity>
- );
- }
-
- _renderItem: Object => React$Element<any> | 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): React$Element<any> | 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 (
- <TouchableOpacity onPress = { this._onPressItem(item) } >
- <View
- pointerEvents = 'box-only'
- style = { styles.itemWrapper }>
- <AvatarListItem
- avatarSize = { AVATAR_SIZE }
- avatarStatus = { item.status }
- avatarStyle = { styles.avatar }
- avatarTextStyle = { styles.avatarText }
- item = { renderableItem }
- key = { index }
- linesStyle = { styles.itemLinesStyle }
- titleStyle = { styles.itemText } />
- { selected && <Icon
- src = { IconCheck }
- style = { styles.selectedIcon } /> }
- </View>
- </TouchableOpacity>
- );
- }
-
- _renderSeparator: () => React$Element<*> | null
-
- /**
- * Renders the item separator.
- *
- * @returns {?React$Element<*>}
- */
- _renderSeparator() {
- return (
- <View style = { styles.separator } />
- );
- }
-
- _renderShareMeetingButton: () => React$Element<any>;
-
- /**
- * Renders a button to share the meeting info.
- *
- * @returns {React#Element<*>}
- */
- _renderShareMeetingButton() {
- const { _headerStyles } = this.props;
-
- return (
- <SafeAreaView
- style = { [
- styles.bottomBar,
- _headerStyles.headerOverlay,
- this.state.bottomPadding ? styles.extraBarPadding : null
- ] }>
- <TouchableOpacity
- onPress = { this._onShareMeeting }>
- <Icon
- src = { IconShare }
- style = { [ _headerStyles.headerButtonText, styles.shareIcon ] } />
- </TouchableOpacity>
- </SafeAreaView>
- );
- }
-
- _setFieldRef: ?TextInput => void
-
- /**
- * Sets a reference to the input field for later use.
- *
- * @param {?TextInput} input - The reference to the input field.
- * @returns {void}
- */
- _setFieldRef(input) {
- this.inputFieldRef = input;
- }
-
- /**
- * 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),
- _headerStyles: ColorSchemeRegistry.get(state, 'Header'),
- _isVisible: state['features/base/modal'].activeModalId === ADD_PEOPLE_DIALOG_VIEW_ID
- };
- }
-
- export default translate(connect(_mapStateToProps)(AddPeopleDialog));
|