[RN] Add invite screen
This commit is contained in:
parent
38b1be1291
commit
b6e2701991
|
@ -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";
|
||||
}
|
||||
|
|
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
|
@ -8,6 +8,7 @@
|
|||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " d="" />
|
||||
<glyph unicode="" 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="" 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="" glyph-name="invite" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
|
||||
<glyph unicode="" glyph-name="add" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
|
||||
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />
|
||||
|
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.ttf
Binary file not shown.
BIN
fonts/jitsi.woff
BIN
fonts/jitsi.woff
Binary file not shown.
File diff suppressed because one or more lines are too long
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -41,7 +41,7 @@ export default class BackButton extends Component<Props> {
|
|||
<Icon
|
||||
name = { 'arrow_back' }
|
||||
style = { [
|
||||
styles.headerButton,
|
||||
styles.headerButtonIcon,
|
||||
this.props.style
|
||||
] } />
|
||||
</TouchableOpacity>
|
||||
|
|
|
@ -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);
|
|
@ -38,7 +38,7 @@ export default class Header extends Component<Props> {
|
|||
* @returns {Object}
|
||||
*/
|
||||
static get buttonStyle(): Object {
|
||||
return styles.headerButton;
|
||||
return styles.headerButtonIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 (
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.headerTextWrapper }>
|
||||
<Text
|
||||
style = { [
|
||||
styles.headerText
|
||||
] }>
|
||||
{ this.props.t(this.props.labelKey) }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}.
|
||||
*
|
||||
|
|
|
@ -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,14 +46,19 @@ 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;
|
||||
|
||||
if (_addPeopleEnabled) {
|
||||
_onOpenAddPeopleDialog();
|
||||
} else {
|
||||
_onShareRoom();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux actions to {@link InviteButton}'s React
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export * from './native';
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export * from './web';
|
|
@ -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));
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export { default as AddPeopleDialog } from './AddPeopleDialog';
|
|
@ -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
|
||||
}
|
||||
};
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export { default as AddPeopleDialog } from './AddPeopleDialog';
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue