Bladeren bron

[RN] Add invite screen

master
Bettenbuk Zoltan 6 jaren geleden
bovenliggende
commit
b6e2701991
30 gewijzigde bestanden met toevoegingen van 1021 en 228 verwijderingen
  1. 12
    0
      css/_font.scss
  2. BIN
      fonts/jitsi.eot
  3. 4
    0
      fonts/jitsi.svg
  4. BIN
      fonts/jitsi.ttf
  5. BIN
      fonts/jitsi.woff
  6. 1
    1
      fonts/selection.json
  7. 10
    0
      lang/main.json
  8. 5
    0
      react/features/base/react/Types.js
  9. 1
    1
      react/features/base/react/components/native/BackButton.js
  10. 70
    0
      react/features/base/react/components/native/ForwardButton.js
  11. 1
    1
      react/features/base/react/components/native/Header.js
  12. 11
    7
      react/features/base/react/components/native/HeaderLabel.js
  13. 1
    0
      react/features/base/react/components/native/index.js
  14. 20
    2
      react/features/base/react/components/native/styles.js
  15. 2
    1
      react/features/conference/components/native/Conference.js
  16. 10
    0
      react/features/invite/actionTypes.js
  17. 17
    0
      react/features/invite/actions.js
  18. 0
    0
      react/features/invite/components/AddPeopleDialog.native.js
  19. 18
    33
      react/features/invite/components/InviteButton.native.js
  20. 222
    0
      react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.js
  21. 3
    0
      react/features/invite/components/add-people-dialog/index.native.js
  22. 3
    0
      react/features/invite/components/add-people-dialog/index.web.js
  23. 469
    0
      react/features/invite/components/add-people-dialog/native/AddPeopleDialog.js
  24. 3
    0
      react/features/invite/components/add-people-dialog/native/index.js
  25. 91
    0
      react/features/invite/components/add-people-dialog/native/styles.js
  26. 31
    172
      react/features/invite/components/add-people-dialog/web/AddPeopleDialog.js
  27. 3
    0
      react/features/invite/components/add-people-dialog/web/index.js
  28. 3
    1
      react/features/invite/components/index.js
  29. 2
    8
      react/features/invite/middleware.web.js
  30. 8
    1
      react/features/invite/reducer.js

+ 12
- 0
css/_font.scss Bestand weergeven

@@ -25,6 +25,18 @@
25 25
     -moz-osx-font-smoothing: grayscale;
26 26
 }
27 27
 
28
+.icon-phone:before {
29
+  content: "\e0cd";
30
+}
31
+.icon-radio_button_unchecked:before {
32
+  content: "\e836";
33
+}
34
+.icon-radio_button_checked:before {
35
+  content: "\e837";
36
+}
37
+.icon-search:before {
38
+  content: "\e8b6";
39
+}
28 40
 .icon-chat-unread:before {
29 41
   content: "\e0b7";
30 42
 }

BIN
fonts/jitsi.eot Bestand weergeven


+ 4
- 0
fonts/jitsi.svg Bestand weergeven

@@ -8,6 +8,7 @@
8 8
 <missing-glyph horiz-adv-x="1024" />
9 9
 <glyph unicode="&#x20;" d="" />
10 10
 <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" />
11
+<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" />
11 12
 <glyph unicode="&#xe145;" glyph-name="invite" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
12 13
 <glyph unicode="&#xe146;" glyph-name="add" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
13 14
 <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 @@
21 22
 <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" />
22 23
 <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" />
23 24
 <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" />
25
+<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" />
26
+<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" />
24 27
 <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" />
25 28
 <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" />
29
+<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" />
26 30
 <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" />
27 31
 <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" />
28 32
 <glyph unicode="&#xe904;" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />

BIN
fonts/jitsi.ttf Bestand weergeven


BIN
fonts/jitsi.woff Bestand weergeven


+ 1
- 1
fonts/selection.json
Diff onderdrukt omdat het te groot bestand
Bestand weergeven


+ 10
- 0
lang/main.json Bestand weergeven

@@ -364,6 +364,16 @@
364 364
         "title": "Share",
365 365
         "tooltip": "Share link and dial-in info for this meeting"
366 366
     },
367
+    "inviteDialog": {
368
+        "alertOk": "Ok",
369
+        "alertText": "Failed to invite some participants.",
370
+        "alertTitle": "Invite",
371
+        "header": "Invite",
372
+        "searchCallOnlyPlaceholder": "Enter phone number",
373
+        "searchPeopleOnlyPlaceholder": "Search for participants",
374
+        "searchPlaceholder": "Participant or phone number",
375
+        "send": "Send"
376
+    },
367 377
     "inlineDialogFailure": {
368 378
         "msg": "We stumbled a bit.",
369 379
         "retry": "Try again",

+ 5
- 0
react/features/base/react/Types.js Bestand weergeven

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

+ 1
- 1
react/features/base/react/components/native/BackButton.js Bestand weergeven

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

+ 70
- 0
react/features/base/react/components/native/ForwardButton.js Bestand weergeven

@@ -0,0 +1,70 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { Text, TouchableOpacity } from 'react-native';
5
+
6
+import { translate } from '../../../i18n';
7
+
8
+import styles from './styles';
9
+
10
+/**
11
+ * The type of the React {@code Component} props of {@link ForwardButton}
12
+ */
13
+type Props = {
14
+
15
+    /**
16
+     * True if the nutton should be disabled.
17
+     */
18
+    disabled: boolean;
19
+
20
+    /**
21
+     * The i18n label key of the button.
22
+     */
23
+    labelKey: string,
24
+
25
+    /**
26
+     * The action to be performed when the button is pressed.
27
+     */
28
+    onPress: Function,
29
+
30
+    /**
31
+     * An external style object passed to the component.
32
+     */
33
+    style?: Object,
34
+
35
+    /**
36
+     * The function to be used to translate i18n labels.
37
+     */
38
+    t: Function
39
+};
40
+
41
+/**
42
+ * A component rendering a forward/next/action button.
43
+ */
44
+class ForwardButton extends Component<Props> {
45
+    /**
46
+     * Implements React's {@link Component#render()}, renders the button.
47
+     *
48
+     * @inheritdoc
49
+     * @returns {ReactElement}
50
+     */
51
+    render() {
52
+        return (
53
+            <TouchableOpacity
54
+                accessibilityLabel = { 'Forward' }
55
+                disabled = { this.props.disabled }
56
+                onPress = { this.props.onPress } >
57
+                <Text
58
+                    style = { [
59
+                        styles.headerButtonText,
60
+                        this.props.disabled && styles.disabledButtonText,
61
+                        this.props.style
62
+                    ] }>
63
+                    { this.props.t(this.props.labelKey) }
64
+                </Text>
65
+            </TouchableOpacity>
66
+        );
67
+    }
68
+}
69
+
70
+export default translate(ForwardButton);

+ 1
- 1
react/features/base/react/components/native/Header.js Bestand weergeven

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

+ 11
- 7
react/features/base/react/components/native/HeaderLabel.js Bestand weergeven

@@ -1,7 +1,7 @@
1 1
 // @flow
2 2
 
3 3
 import React, { Component } from 'react';
4
-import { Text } from 'react-native';
4
+import { Text, View } from 'react-native';
5 5
 
6 6
 import { translate } from '../../../i18n';
7 7
 
@@ -35,12 +35,16 @@ class HeaderLabel extends Component<Props> {
35 35
      */
36 36
     render() {
37 37
         return (
38
-            <Text
39
-                style = { [
40
-                    styles.headerText
41
-                ] }>
42
-                { this.props.t(this.props.labelKey) }
43
-            </Text>
38
+            <View
39
+                pointerEvents = 'box-none'
40
+                style = { styles.headerTextWrapper }>
41
+                <Text
42
+                    style = { [
43
+                        styles.headerText
44
+                    ] }>
45
+                    { this.props.t(this.props.labelKey) }
46
+                </Text>
47
+            </View>
44 48
         );
45 49
     }
46 50
 }

+ 1
- 0
react/features/base/react/components/native/index.js Bestand weergeven

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

+ 20
- 2
react/features/base/react/components/native/styles.js Bestand weergeven

@@ -19,16 +19,26 @@ export const SIDEBAR_WIDTH = 250;
19 19
 export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
20 20
 
21 21
 const HEADER_STYLES = {
22
+
23
+    disabledButtonText: {
24
+        opacity: 0.6
25
+    },
26
+
22 27
     /**
23 28
      * Platform specific header button (e.g. back, menu, etc).
24 29
      */
25
-    headerButton: {
30
+    headerButtonIcon: {
26 31
         alignSelf: 'center',
27 32
         color: ColorPalette.white,
28 33
         fontSize: 26,
29 34
         paddingRight: 22
30 35
     },
31 36
 
37
+    headerButtonText: {
38
+        color: ColorPalette.white,
39
+        fontSize: 20
40
+    },
41
+
32 42
     /**
33 43
      * Style of the header overlay to cover the unsafe areas.
34 44
      */
@@ -44,6 +54,14 @@ const HEADER_STYLES = {
44 54
         fontSize: 20
45 55
     },
46 56
 
57
+    headerTextWrapper: {
58
+        alignItems: 'center',
59
+        justifyContent: 'center',
60
+        left: 0,
61
+        position: 'absolute',
62
+        right: 0
63
+    },
64
+
47 65
     /**
48 66
      * The top-level element of a page.
49 67
      */
@@ -63,7 +81,7 @@ const HEADER_STYLES = {
63 81
         backgroundColor: HEADER_COLOR,
64 82
         flexDirection: 'row',
65 83
         height: HEADER_HEIGHT,
66
-        justifyContent: 'flex-start',
84
+        justifyContent: 'space-between',
67 85
         padding: HEADER_PADDING
68 86
     }
69 87
 };

+ 2
- 1
react/features/conference/components/native/Conference.js Bestand weergeven

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

+ 10
- 0
react/features/invite/actionTypes.js Bestand weergeven

@@ -41,6 +41,16 @@ export const REMOVE_PENDING_INVITE_REQUESTS
41 41
  */
42 42
 export const SET_CALLEE_INFO_VISIBLE = Symbol('SET_CALLEE_INFO_VISIBLE');
43 43
 
44
+/**
45
+ * The type of redux action which sets the invite dialog visible or invisible.
46
+ *
47
+ * {
48
+ *     type: SET_INVITE_DIALOG_VISIBLE,
49
+ *     visible: boolean
50
+ * }
51
+ */
52
+export const SET_INVITE_DIALOG_VISIBLE = Symbol('SET_INVITE_DIALOG_VISIBLE');
53
+
44 54
 /**
45 55
  * The type of the action which signals an error occurred while requesting dial-
46 56
  * in numbers.

+ 17
- 0
react/features/invite/actions.js Bestand weergeven

@@ -9,6 +9,7 @@ import {
9 9
     BEGIN_ADD_PEOPLE,
10 10
     REMOVE_PENDING_INVITE_REQUESTS,
11 11
     SET_CALLEE_INFO_VISIBLE,
12
+    SET_INVITE_DIALOG_VISIBLE,
12 13
     UPDATE_DIAL_IN_NUMBERS_FAILED,
13 14
     UPDATE_DIAL_IN_NUMBERS_SUCCESS
14 15
 } from './actionTypes';
@@ -197,6 +198,22 @@ export function updateDialInNumbers() {
197 198
     };
198 199
 }
199 200
 
201
+/**
202
+ * Sets the visibility of the invite dialog.
203
+ *
204
+ * @param {boolean} visible - The visibility to set.
205
+ * @returns {{
206
+ *     type: SET_INVITE_DIALOG_VISIBLE,
207
+ *     visible: boolean
208
+ * }}
209
+ */
210
+export function setAddPeopleDialogVisible(visible: boolean) {
211
+    return {
212
+        type: SET_INVITE_DIALOG_VISIBLE,
213
+        visible
214
+    };
215
+}
216
+
200 217
 /**
201 218
  * Sets the visibility of {@code CalleeInfo}.
202 219
  *

+ 0
- 0
react/features/invite/components/AddPeopleDialog.native.js Bestand weergeven


+ 18
- 33
react/features/invite/components/InviteButton.native.js Bestand weergeven

@@ -8,29 +8,21 @@ import { AbstractButton } from '../../base/toolbox';
8 8
 import type { AbstractButtonProps } from '../../base/toolbox';
9 9
 import { beginShareRoom } from '../../share-room';
10 10
 
11
-import { beginAddPeople } from '../actions';
11
+import { setAddPeopleDialogVisible } from '../actions';
12 12
 import { isAddPeopleEnabled, isDialOutEnabled } from '../functions';
13 13
 
14 14
 type Props = AbstractButtonProps & {
15 15
 
16 16
     /**
17
-     * Whether or not the feature to directly invite people into the
17
+     * Whether or not the feature to invite people to join the
18 18
      * conference is available.
19 19
      */
20 20
     _addPeopleEnabled: boolean,
21 21
 
22 22
     /**
23
-     * Whether or not the feature to dial out to number to join the
24
-     * conference is available.
23
+     * Opens the add people dialog.
25 24
      */
26
-    _dialOutEnabled: boolean,
27
-
28
-    /**
29
-     * Launches native invite dialog.
30
-     *
31
-     * @protected
32
-     */
33
-    _onAddPeople: Function,
25
+    _onOpenAddPeopleDialog: Function,
34 26
 
35 27
     /**
36 28
      * Begins the UI procedure to share the conference/room URL.
@@ -54,12 +46,17 @@ class InviteButton extends AbstractButton<Props, *> {
54 46
      * @returns {void}
55 47
      */
56 48
     _handleClick() {
57
-        // FIXME: dispatch _onAddPeople here, when we have a dialog for it.
58 49
         const {
50
+            _addPeopleEnabled,
51
+            _onOpenAddPeopleDialog,
59 52
             _onShareRoom
60 53
         } = this.props;
61 54
 
62
-        _onShareRoom();
55
+        if (_addPeopleEnabled) {
56
+            _onOpenAddPeopleDialog();
57
+        } else {
58
+            _onShareRoom();
59
+        }
63 60
     }
64 61
 }
65 62
 
@@ -69,22 +66,23 @@ class InviteButton extends AbstractButton<Props, *> {
69 66
  *
70 67
  * @param {Function} dispatch - The redux action {@code dispatch} function.
71 68
  * @returns {{
72
- *     _onAddPeople,
69
+ *     _onOpenAddPeopleDialog,
73 70
  *     _onShareRoom
74 71
  * }}
75 72
  * @private
76 73
  */
77 74
 function _mapDispatchToProps(dispatch: Dispatch<*>) {
78 75
     return {
76
+
79 77
         /**
80
-         * Launches the add people dialog.
78
+         * Opens the add people dialog.
81 79
          *
82 80
          * @private
83 81
          * @returns {void}
84 82
          * @type {Function}
85 83
          */
86
-        _onAddPeople() {
87
-            dispatch(beginAddPeople());
84
+        _onOpenAddPeopleDialog() {
85
+            dispatch(setAddPeopleDialogVisible(true));
88 86
         },
89 87
 
90 88
         /**
@@ -107,25 +105,12 @@ function _mapDispatchToProps(dispatch: Dispatch<*>) {
107 105
  * @param {Object} state - The redux store/state.
108 106
  * @private
109 107
  * @returns {{
108
+ *   _addPeopleEnabled: boolean
110 109
  * }}
111 110
  */
112 111
 function _mapStateToProps(state) {
113 112
     return {
114
-        /**
115
-         * Whether or not the feature to directly invite people into the
116
-         * conference is available.
117
-         *
118
-         * @type {boolean}
119
-         */
120
-        _addPeopleEnabled: isAddPeopleEnabled(state),
121
-
122
-        /**
123
-         * Whether or not the feature to dial out to number to join the
124
-         * conference is available.
125
-         *
126
-         * @type {boolean}
127
-         */
128
-        _dialOutEnabled: isDialOutEnabled(state)
113
+        _addPeopleEnabled: isAddPeopleEnabled(state) || isDialOutEnabled(state)
129 114
     };
130 115
 }
131 116
 

+ 222
- 0
react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.js Bestand weergeven

@@ -0,0 +1,222 @@
1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+import { createInviteDialogEvent, sendAnalytics } from '../../../analytics';
6
+
7
+import { invite } from '../../actions';
8
+import {
9
+    getInviteResultsForQuery,
10
+    getInviteTypeCounts,
11
+    isAddPeopleEnabled,
12
+    isDialOutEnabled
13
+} from '../../functions';
14
+
15
+const logger = require('jitsi-meet-logger').getLogger(__filename);
16
+
17
+export type Props = {
18
+
19
+    /**
20
+     * Whether or not to show Add People functionality.
21
+     */
22
+    _addPeopleEnabled: boolean,
23
+
24
+    /**
25
+     * The URL for validating if a phone number can be called.
26
+     */
27
+    _dialOutAuthUrl: string,
28
+
29
+    /**
30
+     * Whether or not to show Dial Out functionality.
31
+     */
32
+    _dialOutEnabled: boolean,
33
+
34
+    /**
35
+     * The JWT token.
36
+     */
37
+    _jwt: string,
38
+
39
+    /**
40
+     * The query types used when searching people.
41
+     */
42
+    _peopleSearchQueryTypes: Array<string>,
43
+
44
+    /**
45
+     * The URL pointing to the service allowing for people search.
46
+     */
47
+    _peopleSearchUrl: string,
48
+
49
+    /**
50
+     * The Redux dispatch function.
51
+     */
52
+    dispatch: Function
53
+};
54
+
55
+export type State = {
56
+
57
+    /**
58
+     * Indicating that an error occurred when adding people to the call.
59
+     */
60
+    addToCallError: boolean,
61
+
62
+    /**
63
+     * Indicating that we're currently adding the new people to the
64
+     * call.
65
+     */
66
+    addToCallInProgress: boolean,
67
+
68
+    /**
69
+     * The list of invite items.
70
+     */
71
+    inviteItems: Array<Object>,
72
+};
73
+
74
+/**
75
+ * Implements an abstract dialog to invite people to the conference.
76
+ */
77
+export default class AbstractAddPeopleDialog<P: Props, S: State>
78
+    extends Component<P, S> {
79
+    /**
80
+     * Constructor of the component.
81
+     *
82
+     * @inheritdoc
83
+     */
84
+    constructor(props: P) {
85
+        super(props);
86
+
87
+        this._query = this._query.bind(this);
88
+    }
89
+
90
+    /**
91
+     * Invite people and numbers to the conference. The logic works by inviting
92
+     * numbers, people/rooms, and videosipgw in parallel. All invitees are
93
+     * stored in an array. As each invite succeeds, the invitee is removed
94
+     * from the array. After all invites finish, close the modal if there are
95
+     * no invites left to send. If any are left, that means an invite failed
96
+     * and an error state should display.
97
+     *
98
+     * @param {Array<Object>} invitees - The items to be invited.
99
+     * @returns {Promise<Array<Object>>}
100
+     */
101
+    _invite(invitees) {
102
+        const inviteTypeCounts = getInviteTypeCounts(invitees);
103
+
104
+        sendAnalytics(createInviteDialogEvent(
105
+            'clicked', 'inviteButton', {
106
+                ...inviteTypeCounts,
107
+                inviteAllowed: this._isAddDisabled()
108
+            }));
109
+
110
+        if (this._isAddDisabled()) {
111
+            return Promise.resolve([]);
112
+        }
113
+
114
+        this.setState({
115
+            addToCallInProgress: true
116
+        });
117
+
118
+        const { dispatch } = this.props;
119
+
120
+        return dispatch(invite(invitees))
121
+            .then(invitesLeftToSend => {
122
+                this.setState({
123
+                    addToCallInProgress: false
124
+                });
125
+
126
+                // If any invites are left that means something failed to send
127
+                // so treat it as an error.
128
+                if (invitesLeftToSend.length) {
129
+                    const erroredInviteTypeCounts
130
+                        = getInviteTypeCounts(invitesLeftToSend);
131
+
132
+                    logger.error(`${invitesLeftToSend.length} invites failed`,
133
+                        erroredInviteTypeCounts);
134
+
135
+                    sendAnalytics(createInviteDialogEvent(
136
+                        'error', 'invite', {
137
+                            ...erroredInviteTypeCounts
138
+                        }));
139
+
140
+                    this.setState({
141
+                        addToCallError: true
142
+                    });
143
+                }
144
+
145
+                return invitesLeftToSend;
146
+            });
147
+    }
148
+
149
+    /**
150
+     * Indicates if the Add button should be disabled.
151
+     *
152
+     * @private
153
+     * @returns {boolean} - True to indicate that the Add button should
154
+     * be disabled, false otherwise.
155
+     */
156
+    _isAddDisabled() {
157
+        return !this.state.inviteItems.length
158
+            || this.state.addToCallInProgress;
159
+    }
160
+
161
+    _query: (string) => Promise<Array<Object>>;
162
+
163
+    /**
164
+     * Performs a people and phone number search request.
165
+     *
166
+     * @param {string} query - The search text.
167
+     * @private
168
+     * @returns {Promise}
169
+     */
170
+    _query(query = '') {
171
+        const {
172
+            _addPeopleEnabled: addPeopleEnabled,
173
+            _dialOutAuthUrl: dialOutAuthUrl,
174
+            _dialOutEnabled: dialOutEnabled,
175
+            _jwt: jwt,
176
+            _peopleSearchQueryTypes: peopleSearchQueryTypes,
177
+            _peopleSearchUrl: peopleSearchUrl
178
+        } = this.props;
179
+        const options = {
180
+            addPeopleEnabled,
181
+            dialOutAuthUrl,
182
+            dialOutEnabled,
183
+            jwt,
184
+            peopleSearchQueryTypes,
185
+            peopleSearchUrl
186
+        };
187
+
188
+        return getInviteResultsForQuery(query, options);
189
+    }
190
+
191
+}
192
+
193
+/**
194
+ * Maps (parts of) the Redux state to the props of this component.
195
+ *
196
+ * @param {Object} state - The Redux state.
197
+ * @private
198
+ * @returns {{
199
+ *     _addPeopleEnabled: boolean,
200
+ *     _dialOutAuthUrl: string,
201
+ *     _dialOutEnabled: boolean,
202
+ *     _jwt: string,
203
+ *     _peopleSearchQueryTypes: Array<string>,
204
+ *     _peopleSearchUrl: string
205
+ * }}
206
+ */
207
+export function _mapStateToProps(state: Object) {
208
+    const {
209
+        dialOutAuthUrl,
210
+        peopleSearchQueryTypes,
211
+        peopleSearchUrl
212
+    } = state['features/base/config'];
213
+
214
+    return {
215
+        _addPeopleEnabled: isAddPeopleEnabled(state),
216
+        _dialOutAuthUrl: dialOutAuthUrl,
217
+        _dialOutEnabled: isDialOutEnabled(state),
218
+        _jwt: state['features/base/jwt'].jwt,
219
+        _peopleSearchQueryTypes: peopleSearchQueryTypes,
220
+        _peopleSearchUrl: peopleSearchUrl
221
+    };
222
+}

+ 3
- 0
react/features/invite/components/add-people-dialog/index.native.js Bestand weergeven

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

+ 3
- 0
react/features/invite/components/add-people-dialog/index.web.js Bestand weergeven

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

+ 469
- 0
react/features/invite/components/add-people-dialog/native/AddPeopleDialog.js Bestand weergeven

@@ -0,0 +1,469 @@
1
+// @flow
2
+
3
+import _ from 'lodash';
4
+import React from 'react';
5
+import {
6
+    ActivityIndicator,
7
+    Alert,
8
+    FlatList,
9
+    SafeAreaView,
10
+    TextInput,
11
+    TouchableOpacity,
12
+    View
13
+} from 'react-native';
14
+import { connect } from 'react-redux';
15
+
16
+import { Icon } from '../../../../base/font-icons';
17
+import { translate } from '../../../../base/i18n';
18
+import {
19
+    AvatarListItem,
20
+    BackButton,
21
+    ForwardButton,
22
+    Header,
23
+    HeaderLabel,
24
+    Modal,
25
+    type Item
26
+} from '../../../../base/react';
27
+
28
+import { setAddPeopleDialogVisible } from '../../../actions';
29
+
30
+import AbstractAddPeopleDialog, {
31
+    type Props as AbstractProps,
32
+    type State as AbstractState,
33
+    _mapStateToProps as _abstractMapStateToProps
34
+} from '../AbstractAddPeopleDialog';
35
+
36
+import styles, {
37
+    AVATAR_SIZE,
38
+    DARK_GREY
39
+} from './styles';
40
+
41
+type Props = AbstractProps & {
42
+
43
+    /**
44
+     * True if the invite dialog should be open, false otherwise.
45
+     */
46
+    _isVisible: boolean,
47
+
48
+    /**
49
+     * Function used to translate i18n labels.
50
+     */
51
+    t: Function
52
+};
53
+
54
+type State = AbstractState & {
55
+
56
+    /**
57
+     * True if a search is in progress, false otherwise.
58
+     */
59
+    searchInprogress: boolean,
60
+
61
+    /**
62
+     * An array of items that are selectable on this dialog. This is usually
63
+     * populated by an async search.
64
+     */
65
+    selectableItems: Array<Object>
66
+};
67
+
68
+/**
69
+ * Implements a special dialog to invite people from a directory service.
70
+ */
71
+class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
72
+    /**
73
+     * Default state object to reset the state to when needed.
74
+     */
75
+    defaultState = {
76
+        addToCallError: false,
77
+        addToCallInProgress: false,
78
+        inviteItems: [],
79
+        searchInprogress: false,
80
+        selectableItems: []
81
+    };
82
+
83
+    /**
84
+     * Ref of the search field.
85
+     */
86
+    inputFieldRef: ?TextInput;
87
+
88
+    /**
89
+     * TimeoutID to delay the search for the time the user is probably typing.
90
+     */
91
+    searchTimeout: TimeoutID;
92
+
93
+    /**
94
+     * Contrustor of the component.
95
+     *
96
+     * @inheritdoc
97
+     */
98
+    constructor(props: Props) {
99
+        super(props);
100
+
101
+        this.state = this.defaultState;
102
+
103
+        this._keyExtractor = this._keyExtractor.bind(this);
104
+        this._renderItem = this._renderItem.bind(this);
105
+        this._renderSeparator = this._renderSeparator.bind(this);
106
+        this._onCloseAddPeopleDialog = this._onCloseAddPeopleDialog.bind(this);
107
+        this._onInvite = this._onInvite.bind(this);
108
+        this._onPressItem = this._onPressItem.bind(this);
109
+        this._onTypeQuery = this._onTypeQuery.bind(this);
110
+        this._setFieldRef = this._setFieldRef.bind(this);
111
+    }
112
+
113
+    /**
114
+     * Implements {@code Component#componentDidUpdate}.
115
+     *
116
+     * @inheritdoc
117
+     */
118
+    componentDidUpdate(prevProps) {
119
+        if (prevProps._isVisible !== this.props._isVisible) {
120
+            // Clear state
121
+            this._clearState();
122
+        }
123
+    }
124
+
125
+    /**
126
+     * Implements {@code Component#render}.
127
+     *
128
+     * @inheritdoc
129
+     */
130
+    render() {
131
+        const {
132
+            _addPeopleEnabled,
133
+            _dialOutEnabled
134
+        } = this.props;
135
+        const { inviteItems } = this.state;
136
+
137
+        let placeholderKey = 'searchPlaceholder';
138
+
139
+        if (!_addPeopleEnabled) {
140
+            placeholderKey = 'searchCallOnlyPlaceholder';
141
+        } else if (!_dialOutEnabled) {
142
+            placeholderKey = 'searchPeopleOnlyPlaceholder';
143
+        }
144
+
145
+        return (
146
+            <Modal
147
+                onRequestClose = { this._onCloseAddPeopleDialog }
148
+                visible = { this.props._isVisible }>
149
+                <Header>
150
+                    <BackButton onPress = { this._onCloseAddPeopleDialog } />
151
+                    <HeaderLabel labelKey = 'inviteDialog.header' />
152
+                    <ForwardButton
153
+                        disabled = { this._isAddDisabled() }
154
+                        labelKey = 'inviteDialog.send'
155
+                        onPress = { this._onInvite } />
156
+                </Header>
157
+                <SafeAreaView style = { styles.dialogWrapper }>
158
+                    <View
159
+                        style = { styles.searchFieldWrapper }>
160
+                        <View style = { styles.searchIconWrapper }>
161
+                            { this.state.searchInprogress
162
+                                ? <ActivityIndicator
163
+                                    color = { DARK_GREY }
164
+                                    size = 'small' />
165
+                                : <Icon
166
+                                    name = { 'search' }
167
+                                    style = { styles.searchIcon } />}
168
+                        </View>
169
+                        <TextInput
170
+                            autoCorrect = { false }
171
+                            editable = { !this.state.searchInprogress }
172
+                            onChangeText = { this._onTypeQuery }
173
+                            placeholder = {
174
+                                this.props.t(`inviteDialog.${placeholderKey}`)
175
+                            }
176
+                            ref = { this._setFieldRef }
177
+                            style = { styles.searchField } />
178
+                    </View>
179
+                    <FlatList
180
+                        ItemSeparatorComponent = { this._renderSeparator }
181
+                        data = { this.state.selectableItems }
182
+                        extraData = { inviteItems }
183
+                        keyExtractor = { this._keyExtractor }
184
+                        renderItem = { this._renderItem }
185
+                        style = { styles.resultList } />
186
+                </SafeAreaView>
187
+            </Modal>
188
+        );
189
+    }
190
+
191
+    /**
192
+     * Clears the dialog content.
193
+     *
194
+     * @returns {void}
195
+     */
196
+    _clearState() {
197
+        this.setState(this.defaultState);
198
+    }
199
+
200
+    _invite: Array<Object> => Promise<Array<Object>>
201
+
202
+    _isAddDisabled: () => boolean;
203
+
204
+    _keyExtractor: Object => string
205
+
206
+    /**
207
+     * Key extractor for the flatlist.
208
+     *
209
+     * @param {Object} item - The flatlist item that we need the key to be
210
+     * generated for.
211
+     * @returns {string}
212
+     */
213
+    _keyExtractor(item) {
214
+        return item.type === 'user' ? item.user_id : item.number;
215
+    }
216
+
217
+    _onCloseAddPeopleDialog: () => void
218
+
219
+    /**
220
+     * Closes the dialog.
221
+     *
222
+     * @returns {void}
223
+     */
224
+    _onCloseAddPeopleDialog() {
225
+        this.props.dispatch(setAddPeopleDialogVisible(false));
226
+    }
227
+
228
+    _onInvite: () => void
229
+
230
+    /**
231
+     * Invites the selected entries.
232
+     *
233
+     * @returns {void}
234
+     */
235
+    _onInvite() {
236
+        this._invite(this.state.inviteItems)
237
+            .then(invitesLeftToSend => {
238
+                if (invitesLeftToSend.length) {
239
+                    this.setState({
240
+                        inviteItems: invitesLeftToSend
241
+                    });
242
+                    this._showFailedInviteAlert();
243
+                } else {
244
+                    this._onCloseAddPeopleDialog();
245
+                }
246
+            });
247
+    }
248
+
249
+    _onPressItem: Item => Function
250
+
251
+    /**
252
+     * Function to preapre a callback for the onPress event of the touchable.
253
+     *
254
+     * @param {Item} item - The item on which onPress was invoked.
255
+     * @returns {Function}
256
+     */
257
+    _onPressItem(item) {
258
+        return () => {
259
+            const { inviteItems } = this.state;
260
+            const finderKey = item.type === 'phone' ? 'number' : 'user_id';
261
+
262
+            if (inviteItems.find(
263
+                    _.matchesProperty(finderKey, item[finderKey]))) {
264
+                // Item is already selected, need to unselect it.
265
+                this.setState({
266
+                    inviteItems: inviteItems.filter(
267
+                        element => item[finderKey] !== element[finderKey])
268
+                });
269
+            } else {
270
+                // Item is not selected yet, need to add to the list.
271
+                this.setState({
272
+                    inviteItems: _.orderBy(
273
+                        inviteItems.concat(item), [ 'name' ], [ 'asc' ])
274
+                });
275
+            }
276
+        };
277
+    }
278
+
279
+    _onTypeQuery: string => void
280
+
281
+    /**
282
+     * Handles the typing event of the text field on the dialog and performs the
283
+     * search.
284
+     *
285
+     * @param {string} query - The query that is typed in the field.
286
+     * @returns {void}
287
+     */
288
+    _onTypeQuery(query) {
289
+        clearTimeout(this.searchTimeout);
290
+        this.searchTimeout = setTimeout(() => {
291
+            this.setState({
292
+                searchInprogress: true
293
+            }, () => {
294
+                this._performSearch(query);
295
+            });
296
+        }, 500);
297
+    }
298
+
299
+    /**
300
+     * Performs the actual search.
301
+     *
302
+     * @param {string} query - The query to search for.
303
+     * @returns {void}
304
+     */
305
+    _performSearch(query) {
306
+        this._query(query).then(results => {
307
+            const { inviteItems } = this.state;
308
+
309
+            let selectableItems = results.filter(result => {
310
+                switch (result.type) {
311
+                case 'phone':
312
+                    return result.allowed && result.number
313
+                        && !inviteItems.find(
314
+                            _.matchesProperty('number', result.number));
315
+                case 'user':
316
+                    return result.user_id && !inviteItems.find(
317
+                        _.matchesProperty('user_id', result.user_id));
318
+                default:
319
+                    return false;
320
+                }
321
+            });
322
+
323
+            selectableItems
324
+                = _.orderBy(
325
+                    this.state.inviteItems.concat(selectableItems),
326
+                    [ 'name' ], [ 'asc' ]);
327
+
328
+            this.setState({
329
+                selectableItems
330
+            });
331
+        })
332
+        .finally(() => {
333
+            this.setState({
334
+                searchInprogress: false
335
+            }, () => {
336
+                this.inputFieldRef && this.inputFieldRef.focus();
337
+            });
338
+        });
339
+    }
340
+
341
+    _query: (string) => Promise<Array<Object>>;
342
+
343
+    _renderItem: Object => ?React$Element<*>
344
+
345
+    /**
346
+     * Renders a single item in the {@code FlatList}.
347
+     *
348
+     * @param {Object} flatListItem - An item of the data array of the
349
+     * {@code FlatList}.
350
+     * @param {number} index - The index of the currently rendered item.
351
+     * @returns {?React$Element<*>}
352
+     */
353
+    _renderItem(flatListItem, index) {
354
+        const { item } = flatListItem;
355
+        const { inviteItems } = this.state;
356
+        let selected = false;
357
+        let renderableItem;
358
+
359
+        switch (item.type) {
360
+        case 'phone':
361
+            selected
362
+                = inviteItems.find(_.matchesProperty('number', item.number));
363
+            renderableItem = {
364
+                avatar: 'phone',
365
+                key: item.number,
366
+                title: item.number
367
+            };
368
+            break;
369
+        case 'user':
370
+            selected
371
+                = inviteItems.find(_.matchesProperty('user_id', item.user_id));
372
+            renderableItem = {
373
+                avatar: item.avatar,
374
+                key: item.user_id,
375
+                title: item.name
376
+            };
377
+            break;
378
+        default:
379
+            return null;
380
+        }
381
+
382
+        return (
383
+            <TouchableOpacity onPress = { this._onPressItem(item) } >
384
+                <View
385
+                    pointerEvents = 'box-only'
386
+                    style = { styles.itemWrapper }>
387
+                    <Icon
388
+                        name = { selected
389
+                            ? 'radio_button_checked'
390
+                            : 'radio_button_unchecked' }
391
+                        style = { styles.radioButton } />
392
+                    <AvatarListItem
393
+                        avatarSize = { AVATAR_SIZE }
394
+                        avatarStyle = { styles.avatar }
395
+                        avatarTextStyle = { styles.avatarText }
396
+                        item = { renderableItem }
397
+                        key = { index }
398
+                        linesStyle = { styles.itemLinesStyle }
399
+                        titleStyle = { styles.itemText } />
400
+                </View>
401
+            </TouchableOpacity>
402
+        );
403
+    }
404
+
405
+    _renderSeparator: () => ?React$Element<*>
406
+
407
+    /**
408
+     * Renders the item separator.
409
+     *
410
+     * @returns {?React$Element<*>}
411
+     */
412
+    _renderSeparator() {
413
+        return (
414
+            <View style = { styles.separator } />
415
+        );
416
+    }
417
+
418
+    _setFieldRef: ?TextInput => void
419
+
420
+    /**
421
+     * Sets a reference to the input field for later use.
422
+     *
423
+     * @param {?TextInput} input - The reference to the input field.
424
+     * @returns {void}
425
+     */
426
+    _setFieldRef(input) {
427
+        this.inputFieldRef = input;
428
+    }
429
+
430
+    /**
431
+     * Shows an alert telling the user that some invitees were failed to be
432
+     * invited.
433
+     *
434
+     * NOTE: We're using an Alert here because we're on a modal and it makes
435
+     * using our dialogs a tad more difficult.
436
+     *
437
+     * @returns {void}
438
+     */
439
+    _showFailedInviteAlert() {
440
+        const { t } = this.props;
441
+
442
+        Alert.alert(
443
+            t('inviteDialog.alertTitle'),
444
+            t('inviteDialog.alertText'),
445
+            [
446
+                {
447
+                    text: t('inviteDialog.alertOk')
448
+                }
449
+            ]
450
+        );
451
+    }
452
+}
453
+
454
+/**
455
+ * Maps part of the Redux state to the props of this component.
456
+ *
457
+ * @param {Object} state - The Redux state.
458
+ * @returns {{
459
+ *     _isVisible: boolean
460
+ * }}
461
+ */
462
+function _mapStateToProps(state: Object) {
463
+    return {
464
+        ..._abstractMapStateToProps(state),
465
+        _isVisible: state['features/invite'].inviteDialogVisible
466
+    };
467
+}
468
+
469
+export default translate(connect(_mapStateToProps)(AddPeopleDialog));

+ 3
- 0
react/features/invite/components/add-people-dialog/native/index.js Bestand weergeven

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

+ 91
- 0
react/features/invite/components/add-people-dialog/native/styles.js Bestand weergeven

@@ -0,0 +1,91 @@
1
+// @flow
2
+
3
+import { ColorPalette } from '../../../../base/styles';
4
+
5
+export const AVATAR_SIZE = 40;
6
+export const DARK_GREY = 'rgb(28, 32, 37)';
7
+export const LIGHT_GREY = 'rgb(209, 219, 232)';
8
+export const ICON_SIZE = 15;
9
+
10
+export default {
11
+    avatar: {
12
+        backgroundColor: LIGHT_GREY
13
+    },
14
+
15
+    avatarText: {
16
+        color: 'rgb(28, 32, 37)',
17
+        fontSize: 12
18
+    },
19
+
20
+    dialogWrapper: {
21
+        alignItems: 'stretch',
22
+        backgroundColor: ColorPalette.white,
23
+        flex: 1,
24
+        flexDirection: 'column'
25
+    },
26
+
27
+    itemLinesStyle: {
28
+        color: 'rgb(118, 136, 152)',
29
+        fontSize: 13
30
+    },
31
+
32
+    itemText: {
33
+        color: DARK_GREY,
34
+        fontSize: 14,
35
+        fontWeight: 'normal'
36
+    },
37
+
38
+    itemWrapper: {
39
+        alignItems: 'center',
40
+        flexDirection: 'row',
41
+        paddingLeft: 5
42
+    },
43
+
44
+    radioButton: {
45
+        color: DARK_GREY,
46
+        fontSize: 16,
47
+        padding: 2
48
+    },
49
+
50
+    resultList: {
51
+        padding: 5
52
+    },
53
+
54
+    searchField: {
55
+        backgroundColor: 'rgb(240, 243, 247)',
56
+        borderBottomRightRadius: 10,
57
+        borderTopRightRadius: 10,
58
+        flex: 1,
59
+        fontSize: 17,
60
+        paddingVertical: 7
61
+    },
62
+
63
+    separator: {
64
+        borderBottomColor: LIGHT_GREY,
65
+        borderBottomWidth: 1,
66
+        marginLeft: 85
67
+    },
68
+
69
+    searchFieldWrapper: {
70
+        alignItems: 'stretch',
71
+        flexDirection: 'row',
72
+        height: 52,
73
+        paddingHorizontal: 15,
74
+        paddingVertical: 8
75
+    },
76
+
77
+    searchIcon: {
78
+        color: DARK_GREY,
79
+        fontSize: ICON_SIZE
80
+    },
81
+
82
+    searchIconWrapper: {
83
+        alignItems: 'center',
84
+        backgroundColor: 'rgb(240, 243, 247)',
85
+        borderBottomLeftRadius: 10,
86
+        borderTopLeftRadius: 10,
87
+        flexDirection: 'row',
88
+        justifyContent: 'center',
89
+        width: ICON_SIZE + 16
90
+    }
91
+};

react/features/invite/components/AddPeopleDialog.web.js → react/features/invite/components/add-people-dialog/web/AddPeopleDialog.js Bestand weergeven

@@ -2,26 +2,27 @@
2 2
 
3 3
 import Avatar from '@atlaskit/avatar';
4 4
 import InlineMessage from '@atlaskit/inline-message';
5
-import React, { Component } from 'react';
5
+import React from 'react';
6 6
 import { connect } from 'react-redux';
7 7
 
8
-import { createInviteDialogEvent, sendAnalytics } from '../../analytics';
9
-import { Dialog, hideDialog } from '../../base/dialog';
10
-import { translate, translateToHTML } from '../../base/i18n';
11
-import { getLocalParticipant } from '../../base/participants';
12
-import { MultiSelectAutocomplete } from '../../base/react';
8
+import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
9
+import { Dialog, hideDialog } from '../../../../base/dialog';
10
+import { translate, translateToHTML } from '../../../../base/i18n';
11
+import { getLocalParticipant } from '../../../../base/participants';
12
+import { MultiSelectAutocomplete } from '../../../../base/react';
13 13
 
14
-import { invite } from '../actions';
15
-import { getInviteResultsForQuery, getInviteTypeCounts } from '../functions';
16
-
17
-const logger = require('jitsi-meet-logger').getLogger(__filename);
14
+import AbstractAddPeopleDialog, {
15
+    type Props as AbstractProps,
16
+    type State,
17
+    _mapStateToProps as _abstractMapStateToProps
18
+} from '../AbstractAddPeopleDialog';
18 19
 
19 20
 declare var interfaceConfig: Object;
20 21
 
21 22
 /**
22 23
  * The type of the React {@code Component} props of {@link AddPeopleDialog}.
23 24
  */
24
-type Props = {
25
+type Props = AbstractProps & {
25 26
 
26 27
     /**
27 28
      * The {@link JitsiMeetConference} which will be used to invite "room"
@@ -29,41 +30,11 @@ type Props = {
29 30
      */
30 31
     _conference: Object,
31 32
 
32
-    /**
33
-     * The URL for validating if a phone number can be called.
34
-     */
35
-    _dialOutAuthUrl: string,
36
-
37 33
     /**
38 34
      * Whether to show a footer text after the search results as a last element.
39 35
      */
40 36
     _footerTextEnabled: boolean,
41 37
 
42
-    /**
43
-     * The JWT token.
44
-     */
45
-    _jwt: string,
46
-
47
-    /**
48
-     * The query types used when searching people.
49
-     */
50
-    _peopleSearchQueryTypes: Array<string>,
51
-
52
-    /**
53
-     * The URL pointing to the service allowing for people search.
54
-     */
55
-    _peopleSearchUrl: string,
56
-
57
-    /**
58
-     * Whether or not to show Add People functionality.
59
-     */
60
-    addPeopleEnabled: boolean,
61
-
62
-    /**
63
-     * Whether or not to show Dial Out functionality.
64
-     */
65
-    dialOutEnabled: boolean,
66
-
67 38
     /**
68 39
      * The redux {@code dispatch} function.
69 40
      */
@@ -75,32 +46,10 @@ type Props = {
75 46
     t: Function,
76 47
 };
77 48
 
78
-/**
79
- * The type of the React {@code Component} state of {@link AddPeopleDialog}.
80
- */
81
-type State = {
82
-
83
-    /**
84
-     * Indicating that an error occurred when adding people to the call.
85
-     */
86
-    addToCallError: boolean,
87
-
88
-    /**
89
-     * Indicating that we're currently adding the new people to the
90
-     * call.
91
-     */
92
-    addToCallInProgress: boolean,
93
-
94
-    /**
95
-     * The list of invite items.
96
-     */
97
-    inviteItems: Array<Object>
98
-};
99
-
100 49
 /**
101 50
  * The dialog that allows to invite people to the call.
102 51
  */
103
-class AddPeopleDialog extends Component<Props, State> {
52
+class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
104 53
     _multiselect = null;
105 54
 
106 55
     _resourceClient: Object;
@@ -121,12 +70,10 @@ class AddPeopleDialog extends Component<Props, State> {
121 70
         super(props);
122 71
 
123 72
         // Bind event handlers so they are only bound once per instance.
124
-        this._isAddDisabled = this._isAddDisabled.bind(this);
125 73
         this._onItemSelected = this._onItemSelected.bind(this);
126 74
         this._onSelectionChange = this._onSelectionChange.bind(this);
127 75
         this._onSubmit = this._onSubmit.bind(this);
128 76
         this._parseQueryResults = this._parseQueryResults.bind(this);
129
-        this._query = this._query.bind(this);
130 77
         this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
131 78
 
132 79
         this._resourceClient = {
@@ -183,25 +130,27 @@ class AddPeopleDialog extends Component<Props, State> {
183 130
      * @returns {ReactElement}
184 131
      */
185 132
     render() {
186
-        const { _footerTextEnabled,
187
-            addPeopleEnabled,
188
-            dialOutEnabled,
189
-            t } = this.props;
133
+        const {
134
+            _addPeopleEnabled,
135
+            _dialOutEnabled,
136
+            _footerTextEnabled,
137
+            t
138
+        } = this.props;
190 139
         let isMultiSelectDisabled = this.state.addToCallInProgress || false;
191 140
         let placeholder;
192 141
         let loadingMessage;
193 142
         let noMatches;
194 143
         let footerText;
195 144
 
196
-        if (addPeopleEnabled && dialOutEnabled) {
145
+        if (_addPeopleEnabled && _dialOutEnabled) {
197 146
             loadingMessage = 'addPeople.loading';
198 147
             noMatches = 'addPeople.noResults';
199 148
             placeholder = 'addPeople.searchPeopleAndNumbers';
200
-        } else if (addPeopleEnabled) {
149
+        } else if (_addPeopleEnabled) {
201 150
             loadingMessage = 'addPeople.loadingPeople';
202 151
             noMatches = 'addPeople.noResults';
203 152
             placeholder = 'addPeople.searchPeople';
204
-        } else if (dialOutEnabled) {
153
+        } else if (_dialOutEnabled) {
205 154
             loadingMessage = 'addPeople.loadingNumber';
206 155
             noMatches = 'addPeople.noValidNumbers';
207 156
             placeholder = 'addPeople.searchNumbers';
@@ -250,19 +199,9 @@ class AddPeopleDialog extends Component<Props, State> {
250 199
         );
251 200
     }
252 201
 
253
-    _isAddDisabled: () => boolean;
202
+    _invite: Array<Object> => Promise<*>
254 203
 
255
-    /**
256
-     * Indicates if the Add button should be disabled.
257
-     *
258
-     * @private
259
-     * @returns {boolean} - True to indicate that the Add button should
260
-     * be disabled, false otherwise.
261
-     */
262
-    _isAddDisabled() {
263
-        return !this.state.inviteItems.length
264
-            || this.state.addToCallInProgress;
265
-    }
204
+    _isAddDisabled: () => boolean;
266 205
 
267 206
     _onItemSelected: (Object) => Object;
268 207
 
@@ -300,12 +239,7 @@ class AddPeopleDialog extends Component<Props, State> {
300 239
     _onSubmit: () => void;
301 240
 
302 241
     /**
303
-     * Invite people and numbers to the conference. The logic works by inviting
304
-     * numbers, people/rooms, and videosipgw in parallel. All invitees are
305
-     * stored in an array. As each invite succeeds, the invitee is removed
306
-     * from the array. After all invites finish, close the modal if there are
307
-     * no invites left to send. If any are left, that means an invite failed
308
-     * and an error state should display.
242
+     * Submits the selection for inviting.
309 243
      *
310 244
      * @private
311 245
      * @returns {void}
@@ -313,45 +247,10 @@ class AddPeopleDialog extends Component<Props, State> {
313 247
     _onSubmit() {
314 248
         const { inviteItems } = this.state;
315 249
         const invitees = inviteItems.map(({ item }) => item);
316
-        const inviteTypeCounts = getInviteTypeCounts(invitees);
317 250
 
318
-        sendAnalytics(createInviteDialogEvent(
319
-            'clicked', 'inviteButton', {
320
-                ...inviteTypeCounts,
321
-                inviteAllowed: this._isAddDisabled()
322
-            }));
323
-
324
-        if (this._isAddDisabled()) {
325
-            return;
326
-        }
327
-
328
-        this.setState({
329
-            addToCallInProgress: true
330
-        });
331
-
332
-        const { dispatch } = this.props;
333
-
334
-        dispatch(invite(invitees))
251
+        this._invite(invitees)
335 252
             .then(invitesLeftToSend => {
336
-                // If any invites are left that means something failed to send
337
-                // so treat it as an error.
338 253
                 if (invitesLeftToSend.length) {
339
-                    const erroredInviteTypeCounts
340
-                        = getInviteTypeCounts(invitesLeftToSend);
341
-
342
-                    logger.error(`${invitesLeftToSend.length} invites failed`,
343
-                        erroredInviteTypeCounts);
344
-
345
-                    sendAnalytics(createInviteDialogEvent(
346
-                        'error', 'invite', {
347
-                            ...erroredInviteTypeCounts
348
-                        }));
349
-
350
-                    this.setState({
351
-                        addToCallInProgress: false,
352
-                        addToCallError: true
353
-                    });
354
-
355 254
                     const unsentInviteIDs
356 255
                         = invitesLeftToSend.map(invitee =>
357 256
                             invitee.id || invitee.number);
@@ -362,15 +261,9 @@ class AddPeopleDialog extends Component<Props, State> {
362 261
                     if (this._multiselect) {
363 262
                         this._multiselect.setSelectedItems(itemsToSelect);
364 263
                     }
365
-
366
-                    return;
264
+                } else {
265
+                    this.props.dispatch(hideDialog());
367 266
                 }
368
-
369
-                this.setState({
370
-                    addToCallInProgress: false
371
-                });
372
-
373
-                dispatch(hideDialog());
374 267
             });
375 268
     }
376 269
 
@@ -442,34 +335,6 @@ class AddPeopleDialog extends Component<Props, State> {
442 335
 
443 336
     _query: (string) => Promise<Array<Object>>;
444 337
 
445
-    /**
446
-     * Performs a people and phone number search request.
447
-     *
448
-     * @param {string} query - The search text.
449
-     * @private
450
-     * @returns {Promise}
451
-     */
452
-    _query(query = '') {
453
-        const {
454
-            addPeopleEnabled,
455
-            dialOutEnabled,
456
-            _dialOutAuthUrl,
457
-            _jwt,
458
-            _peopleSearchQueryTypes,
459
-            _peopleSearchUrl
460
-        } = this.props;
461
-        const options = {
462
-            dialOutAuthUrl: _dialOutAuthUrl,
463
-            addPeopleEnabled,
464
-            dialOutEnabled,
465
-            jwt: _jwt,
466
-            peopleSearchQueryTypes: _peopleSearchQueryTypes,
467
-            peopleSearchUrl: _peopleSearchUrl
468
-        };
469
-
470
-        return getInviteResultsForQuery(query, options);
471
-    }
472
-
473 338
     /**
474 339
      * Renders the error message if the add doesn't succeed.
475 340
      *
@@ -557,10 +422,7 @@ class AddPeopleDialog extends Component<Props, State> {
557 422
  */
558 423
 function _mapStateToProps(state) {
559 424
     const {
560
-        dialOutAuthUrl,
561
-        enableFeaturesBasedOnToken,
562
-        peopleSearchQueryTypes,
563
-        peopleSearchUrl
425
+        enableFeaturesBasedOnToken
564 426
     } = state['features/base/config'];
565 427
     let footerTextEnabled = false;
566 428
 
@@ -573,11 +435,8 @@ function _mapStateToProps(state) {
573 435
     }
574 436
 
575 437
     return {
576
-        _dialOutAuthUrl: dialOutAuthUrl,
577
-        _footerTextEnabled: footerTextEnabled,
578
-        _jwt: state['features/base/jwt'].jwt,
579
-        _peopleSearchQueryTypes: peopleSearchQueryTypes,
580
-        _peopleSearchUrl: peopleSearchUrl
438
+        ..._abstractMapStateToProps(state),
439
+        _footerTextEnabled: footerTextEnabled
581 440
     };
582 441
 }
583 442
 

+ 3
- 0
react/features/invite/components/add-people-dialog/web/index.js Bestand weergeven

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

+ 3
- 1
react/features/invite/components/index.js Bestand weergeven

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

+ 2
- 8
react/features/invite/middleware.web.js Bestand weergeven

@@ -5,7 +5,6 @@ import { MiddlewareRegistry } from '../base/redux';
5 5
 
6 6
 import { BEGIN_ADD_PEOPLE } from './actionTypes';
7 7
 import { AddPeopleDialog } from './components';
8
-import { isAddPeopleEnabled, isDialOutEnabled } from './functions';
9 8
 import './middleware.any';
10 9
 
11 10
 /**
@@ -36,15 +35,10 @@ MiddlewareRegistry.register(store => next => action => {
36 35
  * @private
37 36
  * @returns {*} The value returned by {@code next(action)}.
38 37
  */
39
-function _beginAddPeople({ dispatch, getState }, next, action) {
38
+function _beginAddPeople({ dispatch }, next, action) {
40 39
     const result = next(action);
41 40
 
42
-    const state = getState();
43
-
44
-    dispatch(openDialog(AddPeopleDialog, {
45
-        addPeopleEnabled: isAddPeopleEnabled(state),
46
-        dialOutEnabled: isDialOutEnabled(state)
47
-    }));
41
+    dispatch(openDialog(AddPeopleDialog));
48 42
 
49 43
     return result;
50 44
 }

+ 8
- 1
react/features/invite/reducer.js Bestand weergeven

@@ -6,6 +6,7 @@ import {
6 6
     ADD_PENDING_INVITE_REQUEST,
7 7
     REMOVE_PENDING_INVITE_REQUESTS,
8 8
     SET_CALLEE_INFO_VISIBLE,
9
+    SET_INVITE_DIALOG_VISIBLE,
9 10
     UPDATE_DIAL_IN_NUMBERS_FAILED,
10 11
     UPDATE_DIAL_IN_NUMBERS_SUCCESS
11 12
 } from './actionTypes';
@@ -20,7 +21,7 @@ const DEFAULT_STATE = {
20 21
      * @type {boolean|undefined}
21 22
      */
22 23
     calleeInfoVisible: false,
23
-
24
+    inviteDialogVisible: false,
24 25
     numbersEnabled: true,
25 26
     pendingInviteRequests: []
26 27
 };
@@ -49,6 +50,12 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
49 50
             initialCalleeInfo: action.initialCalleeInfo
50 51
         };
51 52
 
53
+    case SET_INVITE_DIALOG_VISIBLE:
54
+        return {
55
+            ...state,
56
+            inviteDialogVisible: action.visible
57
+        };
58
+
52 59
     case UPDATE_DIAL_IN_NUMBERS_FAILED:
53 60
         return {
54 61
             ...state,

Laden…
Annuleren
Opslaan