[RN] Add invite screen

This commit is contained in:
Bettenbuk Zoltan 2019-02-26 11:41:57 +01:00 committed by Saúl Ibarra Corretgé
parent 38b1be1291
commit b6e2701991
30 changed files with 1021 additions and 228 deletions

View File

@ -25,6 +25,18 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-phone:before {
content: "\e0cd";
}
.icon-radio_button_unchecked:before {
content: "\e836";
}
.icon-radio_button_checked:before {
content: "\e837";
}
.icon-search:before {
content: "\e8b6";
}
.icon-chat-unread:before {
content: "\e0b7";
}

Binary file not shown.

View File

@ -8,6 +8,7 @@
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" d="" />
<glyph unicode="&#xe0b7;" glyph-name="chat-unread" d="M768 682v86h-512v-86h512zM598 426v86h-342v-86h342zM256 640v-86h512v86h-512zM854 938c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />
<glyph unicode="&#xe0cd;" glyph-name="phone" d="M282 564c62-120 162-220 282-282l94 94c12 12 30 16 44 10 48-16 100-24 152-24 24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44z" />
<glyph unicode="&#xe145;" glyph-name="invite" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
<glyph unicode="&#xe146;" glyph-name="add" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
<glyph unicode="&#xe1aa;" glyph-name="bluetooth" d="M550 328l-80 82v-162zM470 776v-162l80 82zM670 696l-184-184 184-184-244-242h-42v324l-196-196-60 60 238 238-238 238 60 60 196-196v324h42zM834 738c40-64 62-142 62-222 0-84-24-160-66-226l-50 50c26 52 42 110 42 172s-16 120-42 172zM608 512l98 98c12-30 20-64 20-98s-8-70-20-100z" />
@ -21,8 +22,11 @@
<glyph unicode="&#xe616;" glyph-name="event_note" d="M598 426v-84h-300v84h300zM810 214v468h-596v-468h596zM810 896c46 0 86-40 86-86v-596c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h42v86h86v-86h340v86h86v-86h42zM726 598v-86h-428v86h428z" />
<glyph unicode="&#xe61d;" glyph-name="phone-talk" d="M640 512c0 70-58 128-128 128v86c118 0 214-96 214-214h-86zM810 512c0 166-132 298-298 298v86c212 0 384-172 384-384h-86zM854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24z" />
<glyph unicode="&#xe80b;" glyph-name="public" d="M764 282c56 60 90 142 90 230 0 142-88 266-214 316v-18c0-46-40-84-86-84h-84v-86c0-24-20-42-44-42h-84v-86h256c24 0 42-18 42-42v-128h42c38 0 70-26 82-60zM470 174v82c-46 0-86 40-86 86v42l-204 204c-6-24-10-50-10-76 0-174 132-318 300-338zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe836;" glyph-name="radio_button_unchecked" d="M512 170c188 0 342 154 342 342s-154 342-342 342-342-154-342-342 154-342 342-342zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe837;" glyph-name="radio_button_checked" d="M512 170c188 0 342 154 342 342s-154 342-342 342-342-154-342-342 154-342 342-342zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426zM512 726c118 0 214-96 214-214s-96-214-214-214-214 96-214 214 96 214 214 214z" />
<glyph unicode="&#xe89e;" glyph-name="open_in_new" d="M598 896h298v-298h-86v152l-418-418-60 60 418 418h-152v86zM810 214v298h86v-298c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h298v-86h-298v-596h596z" />
<glyph unicode="&#xe8b3;" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
<glyph unicode="&#xe8b6;" glyph-name="search" d="M406 426c106 0 192 86 192 192s-86 192-192 192-192-86-192-192 86-192 192-192zM662 426l212-212-64-64-212 212v34l-12 12c-48-42-112-66-180-66-154 0-278 122-278 276s124 278 278 278 276-124 276-278c0-68-24-132-66-180l12-12h34z" />
<glyph unicode="&#xe900;" glyph-name="AUD" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM308.25 387.3h57.225l-87.675 252.525h-62.125l-87.675-252.525h53.025l19.425 60.2h88.725l19.075-60.2zM461.9 639.825h-52.85v-165.375c0-56 41.125-93.625 105.7-93.625 64.75 0 105.875 37.625 105.875 93.625v165.375h-52.85v-159.95c0-31.85-19.075-52.15-53.025-52.15-33.775 0-52.85 20.3-52.85 52.15v159.95zM682.225 640v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM735.075 594.85v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15zM243.5 587.325l-31.675-99.050h66.15l-31.325 99.050h-3.15z" />
<glyph unicode="&#xe903;" glyph-name="mic-camera-combined" d="M756.704 628.138l267.296 202.213v-635.075l-267.296 202.213v-191.923c0-12.085-11.296-21.863-25.216-21.863h-706.272c-13.92 0-25.216 9.777-25.216 21.863v612.25c0 12.085 11.296 21.863 25.216 21.863h706.272c13.92 0 25.216-9.777 25.216-21.863v-189.679zM371.338 376.228c47.817 0 86.529 40.232 86.529 89.811v184.835c0 49.651-38.713 89.883-86.529 89.883-47.788 0-86.515-40.232-86.515-89.883v-184.835c0-49.579 38.756-89.811 86.515-89.811v0zM356.754 314.070v-32.78h33.718v33.412c73.858 9.606 131.235 73.73 131.235 151.351v88.232h-30.636v-88.232c0-67.57-53.696-122.534-119.734-122.534-66.024 0-119.691 54.964-119.691 122.534v88.232h-30.636v-88.232c0-79.215 59.674-144.502 135.744-151.969v-0.014z" />
<glyph unicode="&#xe904;" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -364,6 +364,16 @@
"title": "Share",
"tooltip": "Share link and dial-in info for this meeting"
},
"inviteDialog": {
"alertOk": "Ok",
"alertText": "Failed to invite some participants.",
"alertTitle": "Invite",
"header": "Invite",
"searchCallOnlyPlaceholder": "Enter phone number",
"searchPeopleOnlyPlaceholder": "Search for participants",
"searchPlaceholder": "Participant or phone number",
"send": "Send"
},
"inlineDialogFailure": {
"msg": "We stumbled a bit.",
"retry": "Try again",

View File

@ -7,6 +7,11 @@ import type { ComponentType, Element } from 'react';
*/
export type Item = {
/**
* The avatar URL or icon name.
*/
avatar: ?string,
/**
* the color base of the avatar
*/

View File

@ -41,7 +41,7 @@ export default class BackButton extends Component<Props> {
<Icon
name = { 'arrow_back' }
style = { [
styles.headerButton,
styles.headerButtonIcon,
this.props.style
] } />
</TouchableOpacity>

View File

@ -0,0 +1,70 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity } from 'react-native';
import { translate } from '../../../i18n';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link ForwardButton}
*/
type Props = {
/**
* True if the nutton should be disabled.
*/
disabled: boolean;
/**
* The i18n label key of the button.
*/
labelKey: string,
/**
* The action to be performed when the button is pressed.
*/
onPress: Function,
/**
* An external style object passed to the component.
*/
style?: Object,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* A component rendering a forward/next/action button.
*/
class ForwardButton extends Component<Props> {
/**
* Implements React's {@link Component#render()}, renders the button.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<TouchableOpacity
accessibilityLabel = { 'Forward' }
disabled = { this.props.disabled }
onPress = { this.props.onPress } >
<Text
style = { [
styles.headerButtonText,
this.props.disabled && styles.disabledButtonText,
this.props.style
] }>
{ this.props.t(this.props.labelKey) }
</Text>
</TouchableOpacity>
);
}
}
export default translate(ForwardButton);

View File

@ -38,7 +38,7 @@ export default class Header extends Component<Props> {
* @returns {Object}
*/
static get buttonStyle(): Object {
return styles.headerButton;
return styles.headerButtonIcon;
}
/**

View File

@ -1,7 +1,7 @@
// @flow
import React, { Component } from 'react';
import { Text } from 'react-native';
import { Text, View } from 'react-native';
import { translate } from '../../../i18n';
@ -35,12 +35,16 @@ class HeaderLabel extends Component<Props> {
*/
render() {
return (
<Text
style = { [
styles.headerText
] }>
{ this.props.t(this.props.labelKey) }
</Text>
<View
pointerEvents = 'box-none'
style = { styles.headerTextWrapper }>
<Text
style = { [
styles.headerText
] }>
{ this.props.t(this.props.labelKey) }
</Text>
</View>
);
}
}

View File

@ -3,6 +3,7 @@
export { default as AvatarListItem } from './AvatarListItem';
export { default as BackButton } from './BackButton';
export { default as Container } from './Container';
export { default as ForwardButton } from './ForwardButton';
export { default as Header } from './Header';
export { default as HeaderLabel } from './HeaderLabel';
export { default as Link } from './Link';

View File

@ -19,16 +19,26 @@ export const SIDEBAR_WIDTH = 250;
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
const HEADER_STYLES = {
disabledButtonText: {
opacity: 0.6
},
/**
* Platform specific header button (e.g. back, menu, etc).
*/
headerButton: {
headerButtonIcon: {
alignSelf: 'center',
color: ColorPalette.white,
fontSize: 26,
paddingRight: 22
},
headerButtonText: {
color: ColorPalette.white,
fontSize: 20
},
/**
* Style of the header overlay to cover the unsafe areas.
*/
@ -44,6 +54,14 @@ const HEADER_STYLES = {
fontSize: 20
},
headerTextWrapper: {
alignItems: 'center',
justifyContent: 'center',
left: 0,
position: 'absolute',
right: 0
},
/**
* The top-level element of a page.
*/
@ -63,7 +81,7 @@ const HEADER_STYLES = {
backgroundColor: HEADER_COLOR,
flexDirection: 'row',
height: HEADER_HEIGHT,
justifyContent: 'flex-start',
justifyContent: 'space-between',
padding: HEADER_PADDING
}
};

View File

@ -22,7 +22,7 @@ import {
TileView
} from '../../../filmstrip';
import { LargeVideo } from '../../../large-video';
import { CalleeInfoContainer } from '../../../invite';
import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
import { Captions } from '../../../subtitles';
import { setToolboxVisible, Toolbox } from '../../../toolbox';
import { shouldDisplayTileView } from '../../../video-layout';
@ -255,6 +255,7 @@ class Conference extends Component<Props> {
translucent = { true } />
<Chat />
<AddPeopleDialog />
{/*
* The LargeVideo is the lowermost stacking layer.

View File

@ -41,6 +41,16 @@ export const REMOVE_PENDING_INVITE_REQUESTS
*/
export const SET_CALLEE_INFO_VISIBLE = Symbol('SET_CALLEE_INFO_VISIBLE');
/**
* The type of redux action which sets the invite dialog visible or invisible.
*
* {
* type: SET_INVITE_DIALOG_VISIBLE,
* visible: boolean
* }
*/
export const SET_INVITE_DIALOG_VISIBLE = Symbol('SET_INVITE_DIALOG_VISIBLE');
/**
* The type of the action which signals an error occurred while requesting dial-
* in numbers.

View File

@ -9,6 +9,7 @@ import {
BEGIN_ADD_PEOPLE,
REMOVE_PENDING_INVITE_REQUESTS,
SET_CALLEE_INFO_VISIBLE,
SET_INVITE_DIALOG_VISIBLE,
UPDATE_DIAL_IN_NUMBERS_FAILED,
UPDATE_DIAL_IN_NUMBERS_SUCCESS
} from './actionTypes';
@ -197,6 +198,22 @@ export function updateDialInNumbers() {
};
}
/**
* Sets the visibility of the invite dialog.
*
* @param {boolean} visible - The visibility to set.
* @returns {{
* type: SET_INVITE_DIALOG_VISIBLE,
* visible: boolean
* }}
*/
export function setAddPeopleDialogVisible(visible: boolean) {
return {
type: SET_INVITE_DIALOG_VISIBLE,
visible
};
}
/**
* Sets the visibility of {@code CalleeInfo}.
*

View File

@ -8,29 +8,21 @@ import { AbstractButton } from '../../base/toolbox';
import type { AbstractButtonProps } from '../../base/toolbox';
import { beginShareRoom } from '../../share-room';
import { beginAddPeople } from '../actions';
import { setAddPeopleDialogVisible } from '../actions';
import { isAddPeopleEnabled, isDialOutEnabled } from '../functions';
type Props = AbstractButtonProps & {
/**
* Whether or not the feature to directly invite people into the
* Whether or not the feature to invite people to join the
* conference is available.
*/
_addPeopleEnabled: boolean,
/**
* Whether or not the feature to dial out to number to join the
* conference is available.
* Opens the add people dialog.
*/
_dialOutEnabled: boolean,
/**
* Launches native invite dialog.
*
* @protected
*/
_onAddPeople: Function,
_onOpenAddPeopleDialog: Function,
/**
* Begins the UI procedure to share the conference/room URL.
@ -54,12 +46,17 @@ class InviteButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
// FIXME: dispatch _onAddPeople here, when we have a dialog for it.
const {
_addPeopleEnabled,
_onOpenAddPeopleDialog,
_onShareRoom
} = this.props;
_onShareRoom();
if (_addPeopleEnabled) {
_onOpenAddPeopleDialog();
} else {
_onShareRoom();
}
}
}
@ -69,22 +66,23 @@ class InviteButton extends AbstractButton<Props, *> {
*
* @param {Function} dispatch - The redux action {@code dispatch} function.
* @returns {{
* _onAddPeople,
* _onOpenAddPeopleDialog,
* _onShareRoom
* }}
* @private
*/
function _mapDispatchToProps(dispatch: Dispatch<*>) {
return {
/**
* Launches the add people dialog.
* Opens the add people dialog.
*
* @private
* @returns {void}
* @type {Function}
*/
_onAddPeople() {
dispatch(beginAddPeople());
_onOpenAddPeopleDialog() {
dispatch(setAddPeopleDialogVisible(true));
},
/**
@ -107,25 +105,12 @@ function _mapDispatchToProps(dispatch: Dispatch<*>) {
* @param {Object} state - The redux store/state.
* @private
* @returns {{
* _addPeopleEnabled: boolean
* }}
*/
function _mapStateToProps(state) {
return {
/**
* Whether or not the feature to directly invite people into the
* conference is available.
*
* @type {boolean}
*/
_addPeopleEnabled: isAddPeopleEnabled(state),
/**
* Whether or not the feature to dial out to number to join the
* conference is available.
*
* @type {boolean}
*/
_dialOutEnabled: isDialOutEnabled(state)
_addPeopleEnabled: isAddPeopleEnabled(state) || isDialOutEnabled(state)
};
}

View File

@ -0,0 +1,222 @@
// @flow
import { Component } from 'react';
import { createInviteDialogEvent, sendAnalytics } from '../../../analytics';
import { invite } from '../../actions';
import {
getInviteResultsForQuery,
getInviteTypeCounts,
isAddPeopleEnabled,
isDialOutEnabled
} from '../../functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
export type Props = {
/**
* Whether or not to show Add People functionality.
*/
_addPeopleEnabled: boolean,
/**
* The URL for validating if a phone number can be called.
*/
_dialOutAuthUrl: string,
/**
* Whether or not to show Dial Out functionality.
*/
_dialOutEnabled: boolean,
/**
* 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,
/**
* The Redux dispatch function.
*/
dispatch: Function
};
export type State = {
/**
* Indicating that an error occurred when adding people to the call.
*/
addToCallError: boolean,
/**
* Indicating that we're currently adding the new people to the
* call.
*/
addToCallInProgress: boolean,
/**
* The list of invite items.
*/
inviteItems: Array<Object>,
};
/**
* Implements an abstract dialog to invite people to the conference.
*/
export default class AbstractAddPeopleDialog<P: Props, S: State>
extends Component<P, S> {
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
this._query = this._query.bind(this);
}
/**
* Invite people and numbers to the conference. The logic works by inviting
* numbers, people/rooms, and videosipgw in parallel. All invitees are
* stored in an array. As each invite succeeds, the invitee is removed
* from the array. After all invites finish, close the modal if there are
* 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>>}
*/
_invite(invitees) {
const inviteTypeCounts = getInviteTypeCounts(invitees);
sendAnalytics(createInviteDialogEvent(
'clicked', 'inviteButton', {
...inviteTypeCounts,
inviteAllowed: this._isAddDisabled()
}));
if (this._isAddDisabled()) {
return Promise.resolve([]);
}
this.setState({
addToCallInProgress: true
});
const { dispatch } = this.props;
return dispatch(invite(invitees))
.then(invitesLeftToSend => {
this.setState({
addToCallInProgress: false
});
// If any invites are left that means something failed to send
// so treat it as an error.
if (invitesLeftToSend.length) {
const erroredInviteTypeCounts
= getInviteTypeCounts(invitesLeftToSend);
logger.error(`${invitesLeftToSend.length} invites failed`,
erroredInviteTypeCounts);
sendAnalytics(createInviteDialogEvent(
'error', 'invite', {
...erroredInviteTypeCounts
}));
this.setState({
addToCallError: true
});
}
return invitesLeftToSend;
});
}
/**
* Indicates if the Add button should be disabled.
*
* @private
* @returns {boolean} - True to indicate that the Add button should
* be disabled, false otherwise.
*/
_isAddDisabled() {
return !this.state.inviteItems.length
|| this.state.addToCallInProgress;
}
_query: (string) => Promise<Array<Object>>;
/**
* Performs a people and phone number search request.
*
* @param {string} query - The search text.
* @private
* @returns {Promise}
*/
_query(query = '') {
const {
_addPeopleEnabled: addPeopleEnabled,
_dialOutAuthUrl: dialOutAuthUrl,
_dialOutEnabled: dialOutEnabled,
_jwt: jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl
} = this.props;
const options = {
addPeopleEnabled,
dialOutAuthUrl,
dialOutEnabled,
jwt,
peopleSearchQueryTypes,
peopleSearchUrl
};
return getInviteResultsForQuery(query, options);
}
}
/**
* Maps (parts of) the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _addPeopleEnabled: boolean,
* _dialOutAuthUrl: string,
* _dialOutEnabled: boolean,
* _jwt: string,
* _peopleSearchQueryTypes: Array<string>,
* _peopleSearchUrl: string
* }}
*/
export function _mapStateToProps(state: Object) {
const {
dialOutAuthUrl,
peopleSearchQueryTypes,
peopleSearchUrl
} = state['features/base/config'];
return {
_addPeopleEnabled: isAddPeopleEnabled(state),
_dialOutAuthUrl: dialOutAuthUrl,
_dialOutEnabled: isDialOutEnabled(state),
_jwt: state['features/base/jwt'].jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl
};
}

View File

@ -0,0 +1,3 @@
// @flow
export * from './native';

View File

@ -0,0 +1,3 @@
// @flow
export * from './web';

View File

@ -0,0 +1,469 @@
// @flow
import _ from 'lodash';
import React from 'react';
import {
ActivityIndicator,
Alert,
FlatList,
SafeAreaView,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import { connect } from 'react-redux';
import { Icon } from '../../../../base/font-icons';
import { translate } from '../../../../base/i18n';
import {
AvatarListItem,
BackButton,
ForwardButton,
Header,
HeaderLabel,
Modal,
type Item
} from '../../../../base/react';
import { setAddPeopleDialogVisible } from '../../../actions';
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 & {
/**
* True if the invite dialog should be open, false otherwise.
*/
_isVisible: boolean,
/**
* Function used to translate i18n labels.
*/
t: Function
};
type State = AbstractState & {
/**
* 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,
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._renderItem = this._renderItem.bind(this);
this._renderSeparator = this._renderSeparator.bind(this);
this._onCloseAddPeopleDialog = this._onCloseAddPeopleDialog.bind(this);
this._onInvite = this._onInvite.bind(this);
this._onPressItem = this._onPressItem.bind(this);
this._onTypeQuery = this._onTypeQuery.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 } = this.state;
let placeholderKey = 'searchPlaceholder';
if (!_addPeopleEnabled) {
placeholderKey = 'searchCallOnlyPlaceholder';
} else if (!_dialOutEnabled) {
placeholderKey = 'searchPeopleOnlyPlaceholder';
}
return (
<Modal
onRequestClose = { this._onCloseAddPeopleDialog }
visible = { this.props._isVisible }>
<Header>
<BackButton onPress = { this._onCloseAddPeopleDialog } />
<HeaderLabel labelKey = 'inviteDialog.header' />
<ForwardButton
disabled = { this._isAddDisabled() }
labelKey = 'inviteDialog.send'
onPress = { this._onInvite } />
</Header>
<SafeAreaView style = { styles.dialogWrapper }>
<View
style = { styles.searchFieldWrapper }>
<View style = { styles.searchIconWrapper }>
{ this.state.searchInprogress
? <ActivityIndicator
color = { DARK_GREY }
size = 'small' />
: <Icon
name = { 'search' }
style = { styles.searchIcon } />}
</View>
<TextInput
autoCorrect = { false }
editable = { !this.state.searchInprogress }
onChangeText = { this._onTypeQuery }
placeholder = {
this.props.t(`inviteDialog.${placeholderKey}`)
}
ref = { this._setFieldRef }
style = { styles.searchField } />
</View>
<FlatList
ItemSeparatorComponent = { this._renderSeparator }
data = { this.state.selectableItems }
extraData = { inviteItems }
keyExtractor = { this._keyExtractor }
renderItem = { this._renderItem }
style = { styles.resultList } />
</SafeAreaView>
</Modal>
);
}
/**
* Clears the dialog content.
*
* @returns {void}
*/
_clearState() {
this.setState(this.defaultState);
}
_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 === 'user' ? item.user_id : item.number;
}
_onCloseAddPeopleDialog: () => void
/**
* Closes the dialog.
*
* @returns {void}
*/
_onCloseAddPeopleDialog() {
this.props.dispatch(setAddPeopleDialogVisible(false));
}
_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._onCloseAddPeopleDialog();
}
});
}
_onPressItem: Item => Function
/**
* Function to preapre 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 === '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.
this.setState({
inviteItems: _.orderBy(
inviteItems.concat(item), [ 'name' ], [ 'asc' ])
});
}
};
}
_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) {
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 => {
const { inviteItems } = this.state;
let selectableItems = results.filter(result => {
switch (result.type) {
case 'phone':
return result.allowed && result.number
&& !inviteItems.find(
_.matchesProperty('number', result.number));
case 'user':
return result.user_id && !inviteItems.find(
_.matchesProperty('user_id', result.user_id));
default:
return false;
}
});
selectableItems
= _.orderBy(
this.state.inviteItems.concat(selectableItems),
[ 'name' ], [ 'asc' ]);
this.setState({
selectableItems
});
})
.finally(() => {
this.setState({
searchInprogress: false
}, () => {
this.inputFieldRef && this.inputFieldRef.focus();
});
});
}
_query: (string) => Promise<Array<Object>>;
_renderItem: Object => ?React$Element<*>
/**
* Renders a single item in the {@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) {
const { item } = flatListItem;
const { inviteItems } = this.state;
let selected = false;
let renderableItem;
switch (item.type) {
case 'phone':
selected
= inviteItems.find(_.matchesProperty('number', item.number));
renderableItem = {
avatar: 'phone',
key: item.number,
title: item.number
};
break;
case 'user':
selected
= inviteItems.find(_.matchesProperty('user_id', item.user_id));
renderableItem = {
avatar: item.avatar,
key: item.user_id,
title: item.name
};
break;
default:
return null;
}
return (
<TouchableOpacity onPress = { this._onPressItem(item) } >
<View
pointerEvents = 'box-only'
style = { styles.itemWrapper }>
<Icon
name = { selected
? 'radio_button_checked'
: 'radio_button_unchecked' }
style = { styles.radioButton } />
<AvatarListItem
avatarSize = { AVATAR_SIZE }
avatarStyle = { styles.avatar }
avatarTextStyle = { styles.avatarText }
item = { renderableItem }
key = { index }
linesStyle = { styles.itemLinesStyle }
titleStyle = { styles.itemText } />
</View>
</TouchableOpacity>
);
}
_renderSeparator: () => ?React$Element<*>
/**
* Renders the item separator.
*
* @returns {?React$Element<*>}
*/
_renderSeparator() {
return (
<View style = { styles.separator } />
);
}
_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() {
const { t } = this.props;
Alert.alert(
t('inviteDialog.alertTitle'),
t('inviteDialog.alertText'),
[
{
text: t('inviteDialog.alertOk')
}
]
);
}
}
/**
* 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),
_isVisible: state['features/invite'].inviteDialogVisible
};
}
export default translate(connect(_mapStateToProps)(AddPeopleDialog));

View File

@ -0,0 +1,3 @@
// @flow
export { default as AddPeopleDialog } from './AddPeopleDialog';

View File

@ -0,0 +1,91 @@
// @flow
import { ColorPalette } from '../../../../base/styles';
export const AVATAR_SIZE = 40;
export const DARK_GREY = 'rgb(28, 32, 37)';
export const LIGHT_GREY = 'rgb(209, 219, 232)';
export const ICON_SIZE = 15;
export default {
avatar: {
backgroundColor: LIGHT_GREY
},
avatarText: {
color: 'rgb(28, 32, 37)',
fontSize: 12
},
dialogWrapper: {
alignItems: 'stretch',
backgroundColor: ColorPalette.white,
flex: 1,
flexDirection: 'column'
},
itemLinesStyle: {
color: 'rgb(118, 136, 152)',
fontSize: 13
},
itemText: {
color: DARK_GREY,
fontSize: 14,
fontWeight: 'normal'
},
itemWrapper: {
alignItems: 'center',
flexDirection: 'row',
paddingLeft: 5
},
radioButton: {
color: DARK_GREY,
fontSize: 16,
padding: 2
},
resultList: {
padding: 5
},
searchField: {
backgroundColor: 'rgb(240, 243, 247)',
borderBottomRightRadius: 10,
borderTopRightRadius: 10,
flex: 1,
fontSize: 17,
paddingVertical: 7
},
separator: {
borderBottomColor: LIGHT_GREY,
borderBottomWidth: 1,
marginLeft: 85
},
searchFieldWrapper: {
alignItems: 'stretch',
flexDirection: 'row',
height: 52,
paddingHorizontal: 15,
paddingVertical: 8
},
searchIcon: {
color: DARK_GREY,
fontSize: ICON_SIZE
},
searchIconWrapper: {
alignItems: 'center',
backgroundColor: 'rgb(240, 243, 247)',
borderBottomLeftRadius: 10,
borderTopLeftRadius: 10,
flexDirection: 'row',
justifyContent: 'center',
width: ICON_SIZE + 16
}
};

View File

@ -2,26 +2,27 @@
import Avatar from '@atlaskit/avatar';
import InlineMessage from '@atlaskit/inline-message';
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import { createInviteDialogEvent, sendAnalytics } from '../../analytics';
import { Dialog, hideDialog } from '../../base/dialog';
import { translate, translateToHTML } from '../../base/i18n';
import { getLocalParticipant } from '../../base/participants';
import { MultiSelectAutocomplete } from '../../base/react';
import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
import { Dialog, hideDialog } from '../../../../base/dialog';
import { translate, translateToHTML } from '../../../../base/i18n';
import { getLocalParticipant } from '../../../../base/participants';
import { MultiSelectAutocomplete } from '../../../../base/react';
import { invite } from '../actions';
import { getInviteResultsForQuery, getInviteTypeCounts } from '../functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
import AbstractAddPeopleDialog, {
type Props as AbstractProps,
type State,
_mapStateToProps as _abstractMapStateToProps
} from '../AbstractAddPeopleDialog';
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of {@link AddPeopleDialog}.
*/
type Props = {
type Props = AbstractProps & {
/**
* The {@link JitsiMeetConference} which will be used to invite "room"
@ -29,41 +30,11 @@ type Props = {
*/
_conference: Object,
/**
* The URL for validating if a phone number can be called.
*/
_dialOutAuthUrl: string,
/**
* Whether to show a footer text after the search results as a last element.
*/
_footerTextEnabled: boolean,
/**
* 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,
/**
* Whether or not to show Add People functionality.
*/
addPeopleEnabled: boolean,
/**
* Whether or not to show Dial Out functionality.
*/
dialOutEnabled: boolean,
/**
* The redux {@code dispatch} function.
*/
@ -75,32 +46,10 @@ type Props = {
t: Function,
};
/**
* The type of the React {@code Component} state of {@link AddPeopleDialog}.
*/
type State = {
/**
* Indicating that an error occurred when adding people to the call.
*/
addToCallError: boolean,
/**
* Indicating that we're currently adding the new people to the
* call.
*/
addToCallInProgress: boolean,
/**
* The list of invite items.
*/
inviteItems: Array<Object>
};
/**
* The dialog that allows to invite people to the call.
*/
class AddPeopleDialog extends Component<Props, State> {
class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
_multiselect = null;
_resourceClient: Object;
@ -121,12 +70,10 @@ class AddPeopleDialog extends Component<Props, State> {
super(props);
// Bind event handlers so they are only bound once per instance.
this._isAddDisabled = this._isAddDisabled.bind(this);
this._onItemSelected = this._onItemSelected.bind(this);
this._onSelectionChange = this._onSelectionChange.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._parseQueryResults = this._parseQueryResults.bind(this);
this._query = this._query.bind(this);
this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
this._resourceClient = {
@ -183,25 +130,27 @@ class AddPeopleDialog extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { _footerTextEnabled,
addPeopleEnabled,
dialOutEnabled,
t } = this.props;
const {
_addPeopleEnabled,
_dialOutEnabled,
_footerTextEnabled,
t
} = this.props;
let isMultiSelectDisabled = this.state.addToCallInProgress || false;
let placeholder;
let loadingMessage;
let noMatches;
let footerText;
if (addPeopleEnabled && dialOutEnabled) {
if (_addPeopleEnabled && _dialOutEnabled) {
loadingMessage = 'addPeople.loading';
noMatches = 'addPeople.noResults';
placeholder = 'addPeople.searchPeopleAndNumbers';
} else if (addPeopleEnabled) {
} else if (_addPeopleEnabled) {
loadingMessage = 'addPeople.loadingPeople';
noMatches = 'addPeople.noResults';
placeholder = 'addPeople.searchPeople';
} else if (dialOutEnabled) {
} else if (_dialOutEnabled) {
loadingMessage = 'addPeople.loadingNumber';
noMatches = 'addPeople.noValidNumbers';
placeholder = 'addPeople.searchNumbers';
@ -250,19 +199,9 @@ class AddPeopleDialog extends Component<Props, State> {
);
}
_isAddDisabled: () => boolean;
_invite: Array<Object> => Promise<*>
/**
* Indicates if the Add button should be disabled.
*
* @private
* @returns {boolean} - True to indicate that the Add button should
* be disabled, false otherwise.
*/
_isAddDisabled() {
return !this.state.inviteItems.length
|| this.state.addToCallInProgress;
}
_isAddDisabled: () => boolean;
_onItemSelected: (Object) => Object;
@ -300,12 +239,7 @@ class AddPeopleDialog extends Component<Props, State> {
_onSubmit: () => void;
/**
* Invite people and numbers to the conference. The logic works by inviting
* numbers, people/rooms, and videosipgw in parallel. All invitees are
* stored in an array. As each invite succeeds, the invitee is removed
* from the array. After all invites finish, close the modal if there are
* no invites left to send. If any are left, that means an invite failed
* and an error state should display.
* Submits the selection for inviting.
*
* @private
* @returns {void}
@ -313,45 +247,10 @@ class AddPeopleDialog extends Component<Props, State> {
_onSubmit() {
const { inviteItems } = this.state;
const invitees = inviteItems.map(({ item }) => item);
const inviteTypeCounts = getInviteTypeCounts(invitees);
sendAnalytics(createInviteDialogEvent(
'clicked', 'inviteButton', {
...inviteTypeCounts,
inviteAllowed: this._isAddDisabled()
}));
if (this._isAddDisabled()) {
return;
}
this.setState({
addToCallInProgress: true
});
const { dispatch } = this.props;
dispatch(invite(invitees))
this._invite(invitees)
.then(invitesLeftToSend => {
// If any invites are left that means something failed to send
// so treat it as an error.
if (invitesLeftToSend.length) {
const erroredInviteTypeCounts
= getInviteTypeCounts(invitesLeftToSend);
logger.error(`${invitesLeftToSend.length} invites failed`,
erroredInviteTypeCounts);
sendAnalytics(createInviteDialogEvent(
'error', 'invite', {
...erroredInviteTypeCounts
}));
this.setState({
addToCallInProgress: false,
addToCallError: true
});
const unsentInviteIDs
= invitesLeftToSend.map(invitee =>
invitee.id || invitee.number);
@ -362,15 +261,9 @@ class AddPeopleDialog extends Component<Props, State> {
if (this._multiselect) {
this._multiselect.setSelectedItems(itemsToSelect);
}
return;
} else {
this.props.dispatch(hideDialog());
}
this.setState({
addToCallInProgress: false
});
dispatch(hideDialog());
});
}
@ -442,34 +335,6 @@ class AddPeopleDialog extends Component<Props, State> {
_query: (string) => Promise<Array<Object>>;
/**
* Performs a people and phone number search request.
*
* @param {string} query - The search text.
* @private
* @returns {Promise}
*/
_query(query = '') {
const {
addPeopleEnabled,
dialOutEnabled,
_dialOutAuthUrl,
_jwt,
_peopleSearchQueryTypes,
_peopleSearchUrl
} = this.props;
const options = {
dialOutAuthUrl: _dialOutAuthUrl,
addPeopleEnabled,
dialOutEnabled,
jwt: _jwt,
peopleSearchQueryTypes: _peopleSearchQueryTypes,
peopleSearchUrl: _peopleSearchUrl
};
return getInviteResultsForQuery(query, options);
}
/**
* Renders the error message if the add doesn't succeed.
*
@ -557,10 +422,7 @@ class AddPeopleDialog extends Component<Props, State> {
*/
function _mapStateToProps(state) {
const {
dialOutAuthUrl,
enableFeaturesBasedOnToken,
peopleSearchQueryTypes,
peopleSearchUrl
enableFeaturesBasedOnToken
} = state['features/base/config'];
let footerTextEnabled = false;
@ -573,11 +435,8 @@ function _mapStateToProps(state) {
}
return {
_dialOutAuthUrl: dialOutAuthUrl,
_footerTextEnabled: footerTextEnabled,
_jwt: state['features/base/jwt'].jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl
..._abstractMapStateToProps(state),
_footerTextEnabled: footerTextEnabled
};
}

View File

@ -0,0 +1,3 @@
// @flow
export { default as AddPeopleDialog } from './AddPeopleDialog';

View File

@ -1,4 +1,6 @@
export { default as AddPeopleDialog } from './AddPeopleDialog';
// @flow
export * from './add-people-dialog';
export { DialInSummary } from './dial-in-summary';
export { default as InfoDialogButton } from './InfoDialogButton';
export { default as InviteButton } from './InviteButton';

View File

@ -5,7 +5,6 @@ import { MiddlewareRegistry } from '../base/redux';
import { BEGIN_ADD_PEOPLE } from './actionTypes';
import { AddPeopleDialog } from './components';
import { isAddPeopleEnabled, isDialOutEnabled } from './functions';
import './middleware.any';
/**
@ -36,15 +35,10 @@ MiddlewareRegistry.register(store => next => action => {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _beginAddPeople({ dispatch, getState }, next, action) {
function _beginAddPeople({ dispatch }, next, action) {
const result = next(action);
const state = getState();
dispatch(openDialog(AddPeopleDialog, {
addPeopleEnabled: isAddPeopleEnabled(state),
dialOutEnabled: isDialOutEnabled(state)
}));
dispatch(openDialog(AddPeopleDialog));
return result;
}

View File

@ -6,6 +6,7 @@ import {
ADD_PENDING_INVITE_REQUEST,
REMOVE_PENDING_INVITE_REQUESTS,
SET_CALLEE_INFO_VISIBLE,
SET_INVITE_DIALOG_VISIBLE,
UPDATE_DIAL_IN_NUMBERS_FAILED,
UPDATE_DIAL_IN_NUMBERS_SUCCESS
} from './actionTypes';
@ -20,7 +21,7 @@ const DEFAULT_STATE = {
* @type {boolean|undefined}
*/
calleeInfoVisible: false,
inviteDialogVisible: false,
numbersEnabled: true,
pendingInviteRequests: []
};
@ -49,6 +50,12 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
initialCalleeInfo: action.initialCalleeInfo
};
case SET_INVITE_DIALOG_VISIBLE:
return {
...state,
inviteDialogVisible: action.visible
};
case UPDATE_DIAL_IN_NUMBERS_FAILED:
return {
...state,