Browse Source

feat(invite): be able to call numbers from the invite dialog (#2555)

* feat(invite): be able to call numbers from the invite dialog

The major changes:
- Remove DialOutDialog, its views, redux hooks, css, and images.
  Its main functionality has been moved into AddPeopleDialog.
- Modify the AppPeopleDialog styling a bit so it is wider.
- Add phone numbers to AddPeopleDialog search results. Phone
  numbers are validated in parallel with the request for people
  and then appended to the result. The validation includes
  an ajax to validate the number is recognized as dialable by
  the server. The trigger for the validation is essentially if
  the entered input is numbers only.
- AddPeopleDialog holds onto the full object representation of
  an item selected in MultiSelectAutocomplete. This is so
  selected items can be removed on successful invite, leaving
  only unsuccessful items.
- More granular error handling on invite so individual invitees
  can be removed from the selected items list.

* squash: change load state, new regex for numbers

* squash: change strings, auto prepend 1 if no country code, add reminders
master
virtuacoplenny 7 years ago
parent
commit
4e4713c3e2

+ 0
- 81
css/_dial-out.scss View File

1
-/**
2
- * The dialog content element.
3
- */
4
-.dial-out-content {
5
-    margin-top: 5px;
6
-
7
-    /**
8
-     * Wrap the contents in flex so items can be aligned on the same line.
9
-     */
10
-    .form-control {
11
-        display: flex;
12
-    }
13
-
14
-   /**
15
-    * The style of the flag icon.
16
-    */
17
-    .dial-out-flag-icon {
18
-        position: absolute;
19
-        left: 5px;
20
-        top: 50%;
21
-        transform: translate(0, -50%);
22
-    }
23
-
24
-    /**
25
-     * The style of the dial code element.
26
-     */
27
-    .dial-out-code {
28
-        margin-bottom: 0;
29
-        padding-left: 25px;
30
-    }
31
-
32
-    /**
33
-     * The dial-out dialog error element.
34
-     */
35
-    .dial-out-error {
36
-        color: $errorColor;
37
-    }
38
-
39
-    /**
40
-     * The style of the dial input element.
41
-     */
42
-    .dial-out-input {
43
-        display: inline-block;
44
-        flex: 1;
45
-        margin-left: 5px;
46
-    }
47
-
48
-    /**
49
-     * Re-styling the default dropdown inside the dial-out-content.
50
-     */
51
-    .dropdown {
52
-        position: relative;
53
-        width: 65px;
54
-    }
55
-
56
-    /**
57
-     * Re-styling the default form-control inside the dial-out-content.
58
-     */
59
-    .form-control {
60
-        margin-bottom: 8px;
61
-    }
62
-
63
-    .dropdown {
64
-        position: relative;
65
-
66
-        input {
67
-            padding-left: 16px;
68
-
69
-            &:read-only {
70
-                color: inherit;
71
-            }
72
-        }
73
-    }
74
-
75
-    .dropdown-trigger-icon {
76
-        position: absolute;
77
-        right: 0;
78
-        top: 50%;
79
-        transform: translate(0, -50%);
80
-    }
81
-}

+ 0
- 35
css/_flag-icon.scss View File

1
-.flag-icon-background {
2
-  background-size: contain;
3
-  background-position: 50%;
4
-  background-repeat: no-repeat;
5
-}
6
-.flag-icon {
7
-  background-size: contain;
8
-  background-position: 50%;
9
-  background-repeat: no-repeat;
10
-  position: relative;
11
-  display: inline-block;
12
-  width: 1.33333333em;
13
-  line-height: 1em;
14
-}
15
-.flag-icon:before {
16
-  content: "\00a0";
17
-}
18
-.flag-icon-au {
19
-    background-image: url(../images/countries/au.svg);
20
-}
21
-.flag-icon-ca {
22
-    background-image: url(../images/countries/ca.svg);
23
-}
24
-.flag-icon-de {
25
-    background-image: url(../images/countries/de.svg);
26
-}
27
-.flag-icon-gb {
28
-    background-image: url(../images/countries/gb.svg);
29
-}
30
-.flag-icon-fr {
31
-    background-image: url(../images/countries/fr.svg);
32
-}
33
-.flag-icon-us {
34
-    background-image: url(../images/countries/us.svg);
35
-}

+ 0
- 3
css/main.scss View File

28
 @import 'font-awesome';
28
 @import 'font-awesome';
29
 /* Fonts END */
29
 /* Fonts END */
30
 
30
 
31
-@import 'flag-icon';
32
-
33
 /* Modules BEGIN */
31
 /* Modules BEGIN */
34
 
32
 
35
-@import 'dial-out';
36
 @import 'aui_reset';
33
 @import 'aui_reset';
37
 @import 'base';
34
 @import 'base';
38
 @import 'utils';
35
 @import 'utils';

+ 4
- 10
css/modals/invite/_add-people.scss View File

11
                 padding-left: 5px;
11
                 padding-left: 5px;
12
             }
12
             }
13
         }
13
         }
14
-    }
15
-}
16
 
14
 
17
-/**
18
- * Styles the loading element in the MultiSelectAutocomplete.
19
- */
20
-.autocomplete-loading {
21
-    justify-content: center;
22
-    display: flex;
23
-    min-width: 260px;
24
-    padding: 20px;
15
+        .add-telephone-icon {
16
+            transform: scaleX(-1);
17
+        }
18
+    }
25
 }
19
 }
26
 
20
 
27
 /**
21
 /**

+ 0
- 9
images/countries/au.svg View File

1
-<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
2
-  <g stroke-width="1pt">
3
-    <path fill="#006" d="M0 0h640v480H0z"/>
4
-    <path d="M0 0v27.95L307.037 250h38.647v-27.95L38.647 0H0zm345.684 0v27.95L38.647 250H0v-27.95L307.037 0h38.647z" fill="#fff"/>
5
-    <path d="M144.035 0v250h57.614V0h-57.615zM0 83.333v83.333h345.684V83.333H0z" fill="#fff"/>
6
-    <path d="M0 100v50h345.684v-50H0zM155.558 0v250h34.568V0h-34.568zM0 250l115.228-83.334h25.765L25.765 250H0zM0 0l115.228 83.333H89.463L0 18.633V0zm204.69 83.333L319.92 0h25.764L230.456 83.333H204.69zM345.685 250l-115.228-83.334h25.765l89.464 64.7V250z" fill="#c00"/>
7
-    <path d="M299.762 392.523l-43.653 3.795 6.013 43.406-30.187-31.764-30.186 31.764 6.014-43.406-43.653-3.795 37.68-22.364-24.244-36.495 40.97 15.514 13.42-41.713 13.42 41.712 40.97-15.515-24.242 36.494m224.444 62.372l-10.537-15.854 17.81 6.742 5.824-18.125 5.825 18.126 17.807-6.742-10.537 15.854 16.37 9.718-18.965 1.65 2.616 18.85-13.116-13.793-13.117 13.794 2.616-18.85-18.964-1.65m16.368-291.815l-10.537-15.856 17.81 6.742 5.824-18.122 5.825 18.12 17.807-6.74-10.537 15.855 16.37 9.717-18.965 1.65 2.616 18.85-13.116-13.793-13.117 13.794 2.616-18.85-18.964-1.65m-89.418 104.883l-10.537-15.853 17.808 6.742 5.825-18.125 5.825 18.125 17.808-6.742-10.536 15.853 16.37 9.72-18.965 1.65 2.615 18.85-13.117-13.795-13.117 13.795 2.617-18.85-18.964-1.65m216.212-37.929l-10.558-15.854 17.822 6.742 5.782-18.125 5.854 18.125 17.772-6.742-10.508 15.854 16.362 9.718-18.97 1.65 2.608 18.85-13.118-13.793-13.117 13.793 2.61-18.85-18.936-1.65m-22.251 73.394l-10.367 6.425 2.914-11.84-9.316-7.863 12.165-.896 4.605-11.29 4.606 11.29 12.165.897-9.317 7.863 2.912 11.84" fill-rule="evenodd" fill="#fff"/>
8
-  </g>
9
-</svg>

+ 0
- 6
images/countries/ca.svg View File

1
-<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
2
-  <g transform="translate(74.118) scale(.9375)">
3
-    <path fill="#fff" d="M81.137 0h362.276v512H81.137z"/>
4
-    <path fill="#bf0a30" d="M-100 0H81.138v512H-100zm543.413 0H624.55v512H443.414zM135.31 247.41l-14.067 4.808 65.456 57.446c4.95 14.764-1.72 19.116-5.97 26.86l71.06-9.02-1.85 71.512 14.718-.423-3.21-70.918 71.13 8.432c-4.402-9.297-8.32-14.233-4.247-29.098l65.414-54.426-11.447-4.144c-9.36-7.222 4.044-34.784 6.066-52.178 0 0-38.195 13.135-40.698 6.262l-9.727-18.685-34.747 38.17c-3.796.91-5.413-.6-6.304-3.808l16.053-79.766-25.42 14.297c-2.128.91-4.256.125-5.658-2.355l-24.45-49.06-25.21 50.95c-1.9 1.826-3.803 2.037-5.382.796l-24.204-13.578 14.53 79.143c-1.156 3.14-3.924 4.025-7.18 2.324l-33.216-37.737c-4.345 6.962-7.29 18.336-13.033 20.885-5.744 2.387-24.98-4.823-37.873-7.637 4.404 15.895 18.176 42.302 9.46 50.957z"/>
5
-  </g>
6
-</svg>

+ 0
- 5
images/countries/de.svg View File

1
-<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
2
-  <path fill="#ffce00" d="M0 320h640v160.002H0z"/>
3
-  <path d="M0 0h640v160H0z"/>
4
-  <path fill="#d00" d="M0 160h640v160H0z"/>
5
-</svg>

+ 0
- 7
images/countries/fr.svg View File

1
-<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
2
-  <g fill-rule="evenodd" stroke-width="1pt">
3
-    <path fill="#fff" d="M0 0h640v480H0z"/>
4
-    <path fill="#00267f" d="M0 0h213.337v480H0z"/>
5
-    <path fill="#f31830" d="M426.662 0H640v480H426.662z"/>
6
-  </g>
7
-</svg>

+ 0
- 15
images/countries/gb.svg View File

1
-<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
2
-  <defs>
3
-    <clipPath id="a">
4
-      <path fill-opacity=".67" d="M-85.333 0h682.67v512h-682.67z"/>
5
-    </clipPath>
6
-  </defs>
7
-  <g clip-path="url(#a)" transform="translate(80) scale(.94)">
8
-    <g stroke-width="1pt">
9
-      <path fill="#006" d="M-256 0H768.02v512.01H-256z"/>
10
-      <path d="M-256 0v57.244l909.535 454.768H768.02V454.77L-141.515 0H-256zM768.02 0v57.243L-141.515 512.01H-256v-57.243L653.535 0H768.02z" fill="#fff"/>
11
-      <path d="M170.675 0v512.01h170.67V0h-170.67zM-256 170.67v170.67H768.02V170.67H-256z" fill="#fff"/>
12
-      <path d="M-256 204.804v102.402H768.02V204.804H-256zM204.81 0v512.01h102.4V0h-102.4zM-256 512.01L85.34 341.34h76.324l-341.34 170.67H-256zM-256 0L85.34 170.67H9.016L-256 38.164V0zm606.356 170.67L691.696 0h76.324L426.68 170.67h-76.324zM768.02 512.01L426.68 341.34h76.324L768.02 473.848v38.162z" fill="#c00"/>
13
-    </g>
14
-  </g>
15
-</svg>

+ 0
- 18
images/countries/us.svg View File

1
-<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
2
-  <g fill-rule="evenodd" transform="scale(.9375)">
3
-    <g stroke-width="1pt">
4
-      <path d="M0 0h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0z" fill="#bd3d44"/>
5
-      <path d="M0 39.385h972.81V78.77H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0z" fill="#fff"/>
6
-    </g>
7
-    <path fill="#192f5d" d="M0 0h389.12v275.69H0z"/>
8
-    <g fill="#fff">
9
-      <path d="M32.427 11.8l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 39.37l3.54 10.896h11.458L70.583 57l3.542 10.897-9.27-6.734-9.269 6.734L59.126 57l-9.269-6.734h11.458zm64.852 0l3.54 10.896h11.457L135.435 57l3.54 10.897-9.268-6.734-9.27 6.734L123.978 57l-9.27-6.734h11.458zm64.855 0l3.54 10.896h11.458L200.29 57l3.541 10.897-9.27-6.734-9.268 6.734L188.833 57l-9.269-6.734h11.457zm64.855 0l3.54 10.896h11.458L265.145 57l3.541 10.897-9.269-6.734-9.27 6.734L253.69 57l-9.27-6.734h11.458zm64.852 0l3.54 10.896h11.457L329.997 57l3.54 10.897-9.268-6.734-9.27 6.734L318.54 57l-9.27-6.734h11.458zM32.427 66.939l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 94.508l3.54 10.897h11.458l-9.27 6.734 3.542 10.897-9.27-6.734-9.269 6.734 3.54-10.897-9.269-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.27-6.734-9.268 6.734 3.54-10.897-9.269-6.734h11.457zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.269-6.734-9.27 6.734 3.542-10.897-9.27-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zM32.427 122.078l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 149.647l3.54 10.897h11.458l-9.27 6.734 3.542 10.897-9.27-6.734-9.269 6.734 3.54-10.897-9.269-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.27-6.734-9.268 6.734 3.54-10.897-9.269-6.734h11.457zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.269-6.734-9.27 6.734 3.542-10.897-9.27-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458z"/>
10
-      <g>
11
-        <path d="M32.427 177.217l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 204.786l3.54 10.897h11.458l-9.27 6.734 3.542 10.897-9.27-6.734-9.269 6.734 3.54-10.897-9.269-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.27-6.734-9.268 6.734 3.54-10.897-9.269-6.734h11.457zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.269-6.734-9.27 6.734 3.542-10.897-9.27-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458z"/>
12
-      </g>
13
-      <g>
14
-        <path d="M32.427 232.356l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458z"/>
15
-      </g>
16
-    </g>
17
-  </g>
18
-</svg>

+ 14
- 7
lang/main.json View File

452
         "qualityButtonTip": "Change received video quality"
452
         "qualityButtonTip": "Change received video quality"
453
     },
453
     },
454
     "dialOut": {
454
     "dialOut": {
455
-        "dial": "Dial",
456
-        "dialOut": "Call a number",
457
-        "statusMessage": "is now __status__",
458
-        "enterPhone": "Enter phone number",
459
-        "phoneNotAllowed": "Oh, we don't support that destination yet! Sorry!"
455
+        "statusMessage": "is now __status__"
460
     },
456
     },
461
     "addPeople": {
457
     "addPeople": {
462
         "add": "Add",
458
         "add": "Add",
459
+        "countryNotSupported": "We do not support this destination yet.",
460
+        "countryReminder": "Calling outside the US? Please make sure you start with the country code!",
461
+        "disabled": "You can't invite people.",
462
+        "invite": "Invite",
463
+        "loading": "Searching for people and phone numbers",
464
+        "loadingNumber": "Validating phone number",
465
+        "loadingPeople": "Searching for people to invite",
463
         "noResults": "No matching search results",
466
         "noResults": "No matching search results",
464
-        "searchPlaceholder": "Search for people and rooms to add",
465
-        "title": "Add people to your call",
467
+        "noValidNumbers": "Please enter a phone number",
468
+        "searchNumbers": "Enter a phone number to invite",
469
+        "searchPeople": "Enter a name to invite",
470
+        "searchPeopleAndNumbers": "Enter a name or phone number to invite",
471
+        "telephone": "Telephone: __number__",
472
+        "title": "Invite people to your meeting",
466
         "failedToAdd": "Failed to add members"
473
         "failedToAdd": "Failed to add members"
467
     },
474
     },
468
     "inlineDialogFailure": {
475
     "inlineDialogFailure": {

+ 21
- 38
react/features/base/react/components/web/MultiSelectAutocomplete.js View File

1
 import { MultiSelectStateless } from '@atlaskit/multi-select';
1
 import { MultiSelectStateless } from '@atlaskit/multi-select';
2
 import AKInlineDialog from '@atlaskit/inline-dialog';
2
 import AKInlineDialog from '@atlaskit/inline-dialog';
3
-import Spinner from '@atlaskit/spinner';
4
 import _debounce from 'lodash/debounce';
3
 import _debounce from 'lodash/debounce';
5
 import PropTypes from 'prop-types';
4
 import PropTypes from 'prop-types';
6
 import React, { Component } from 'react';
5
 import React, { Component } from 'react';
28
          */
27
          */
29
         isDisabled: PropTypes.bool,
28
         isDisabled: PropTypes.bool,
30
 
29
 
30
+        /**
31
+         * Text to display while a query is executing.
32
+         */
33
+        loadingMessage: PropTypes.string,
34
+
31
         /**
35
         /**
32
          * The text to show when no matches are found.
36
          * The text to show when no matches are found.
33
          */
37
          */
34
         noMatchesFound: PropTypes.string,
38
         noMatchesFound: PropTypes.string,
35
 
39
 
40
+        /**
41
+         * The function called immediately before a selection has been actually
42
+         * selected. Provides an opportunity to do any formatting.
43
+         */
44
+        onItemSelected: PropTypes.func,
45
+
36
         /**
46
         /**
37
          * The function called when the selection changes.
47
          * The function called when the selection changes.
38
          */
48
          */
113
     }
123
     }
114
 
124
 
115
     /**
125
     /**
116
-     * Clears the selected items.
126
+     * Sets the items to display as selected.
117
      *
127
      *
128
+     * @param {Array<Object>} selectedItems - The list of items to display as
129
+     * having been selected.
118
      * @returns {void}
130
      * @returns {void}
119
      */
131
      */
120
-    clear() {
121
-        this.setState({
122
-            selectedItems: []
123
-        });
132
+    setSelectedItems(selectedItems = []) {
133
+        this.setState({ selectedItems });
124
     }
134
     }
125
 
135
 
126
     /**
136
     /**
140
                 <MultiSelectStateless
150
                 <MultiSelectStateless
141
                     filterValue = { this.state.filterValue }
151
                     filterValue = { this.state.filterValue }
142
                     isDisabled = { isDisabled }
152
                     isDisabled = { isDisabled }
153
+                    isLoading = { this.state.loading }
143
                     isOpen = { this.state.isOpen }
154
                     isOpen = { this.state.isOpen }
144
                     items = { this.state.items }
155
                     items = { this.state.items }
156
+                    loadingMessage = { this.props.loadingMessage }
145
                     noMatchesFound = { noMatchesFound }
157
                     noMatchesFound = { noMatchesFound }
146
                     onFilterChange = { this._onFilterChange }
158
                     onFilterChange = { this._onFilterChange }
147
                     onRemoved = { this._onSelectionChange }
159
                     onRemoved = { this._onSelectionChange }
150
                     selectedItems = { this.state.selectedItems }
162
                     selectedItems = { this.state.selectedItems }
151
                     shouldFitContainer = { shouldFitContainer }
163
                     shouldFitContainer = { shouldFitContainer }
152
                     shouldFocus = { shouldFocus } />
164
                     shouldFocus = { shouldFocus } />
153
-                { this._renderLoadingIndicator() }
154
                 { this._renderError() }
165
                 { this._renderError() }
155
             </div>
166
             </div>
156
         );
167
         );
169
             error: this.state.error && Boolean(filterValue),
180
             error: this.state.error && Boolean(filterValue),
170
             filterValue,
181
             filterValue,
171
             isOpen: Boolean(this.state.items.length) && Boolean(filterValue),
182
             isOpen: Boolean(this.state.items.length) && Boolean(filterValue),
172
-            items: filterValue ? this.state.items : []
183
+            items: filterValue ? this.state.items : [],
184
+            loading: Boolean(filterValue)
173
         });
185
         });
174
         if (filterValue) {
186
         if (filterValue) {
175
             this._sendQuery(filterValue);
187
             this._sendQuery(filterValue);
201
         if (existing) {
213
         if (existing) {
202
             selectedItems = selectedItems.filter(k => k !== existing);
214
             selectedItems = selectedItems.filter(k => k !== existing);
203
         } else {
215
         } else {
204
-            selectedItems.push(item);
216
+            selectedItems.push(this.props.onItemSelected(item));
205
         }
217
         }
206
         this.setState({
218
         this.setState({
207
             isOpen: false,
219
             isOpen: false,
236
         );
248
         );
237
     }
249
     }
238
 
250
 
239
-    /**
240
-     * Renders the loading indicator.
241
-     *
242
-     * @returns {ReactElement|null}
243
-     */
244
-    _renderLoadingIndicator() {
245
-        if (!(this.state.loading
246
-            && !this.state.items.length
247
-            && this.state.filterValue.length)) {
248
-            return null;
249
-        }
250
-
251
-        const content = ( // eslint-disable-line no-extra-parens
252
-            <div className = 'autocomplete-loading'>
253
-                <Spinner
254
-                    isCompleting = { false }
255
-                    size = 'medium' />
256
-            </div>
257
-        );
258
-
259
-        return (
260
-            <AKInlineDialog
261
-                content = { content }
262
-                isOpen = { true } />
263
-        );
264
-    }
265
-
266
     /**
251
     /**
267
      * Sends a query to the resourceClient.
252
      * Sends a query to the resourceClient.
268
      *
253
      *
275
         }
260
         }
276
 
261
 
277
         this.setState({
262
         this.setState({
278
-            loading: true,
279
             error: false
263
             error: false
280
         });
264
         });
281
 
265
 
288
             .then(results => {
272
             .then(results => {
289
                 if (this.state.filterValue !== filterValue) {
273
                 if (this.state.filterValue !== filterValue) {
290
                     this.setState({
274
                     this.setState({
291
-                        loading: false,
292
                         error: false
275
                         error: false
293
                     });
276
                     });
294
 
277
 

+ 0
- 46
react/features/dial-out/actionTypes.js View File

1
-/**
2
- * The type of the action which signals a check for a dial-out phone number has
3
- * succeeded.
4
- *
5
- * {
6
- *     type: PHONE_NUMBER_CHECKED,
7
- *     response: Object
8
- * }
9
- */
10
-export const PHONE_NUMBER_CHECKED
11
-    = Symbol('PHONE_NUMBER_CHECKED');
12
-
13
-/**
14
- * The type of the action which signals a cancel of the dial-out operation.
15
- *
16
- * {
17
- *     type: DIAL_OUT_CANCELED,
18
- *     response: Object
19
- * }
20
- */
21
-export const DIAL_OUT_CANCELED
22
-    = Symbol('DIAL_OUT_CANCELED');
23
-
24
-/**
25
- * The type of the action which signals a request for dial-out country codes has
26
- * succeeded.
27
- *
28
- * {
29
- *     type: DIAL_OUT_CODES_UPDATED,
30
- *     response: Object
31
- * }
32
- */
33
-export const DIAL_OUT_CODES_UPDATED
34
-    = Symbol('DIAL_OUT_CODES_UPDATED');
35
-
36
-/**
37
- * The type of the action which signals a failure in some of dial-out service
38
- * requests.
39
- *
40
- * {
41
- *     type: DIAL_OUT_SERVICE_FAILED,
42
- *     response: Object
43
- * }
44
- */
45
-export const DIAL_OUT_SERVICE_FAILED
46
-    = Symbol('DIAL_OUT_SERVICE_FAILED');

+ 0
- 102
react/features/dial-out/actions.js View File

1
-// @flow
2
-
3
-import {
4
-    DIAL_OUT_CANCELED,
5
-    DIAL_OUT_CODES_UPDATED,
6
-    DIAL_OUT_SERVICE_FAILED,
7
-    PHONE_NUMBER_CHECKED
8
-} from './actionTypes';
9
-
10
-declare var $: Function;
11
-declare var config: Object;
12
-
13
-/**
14
- * Dials the given number.
15
- *
16
- * @returns {Function}
17
- */
18
-export function cancel() {
19
-    return {
20
-        type: DIAL_OUT_CANCELED
21
-    };
22
-}
23
-
24
-/**
25
- * Dials the given number.
26
- *
27
- * @param {string} dialNumber - The number to dial.
28
- * @returns {Function}
29
- */
30
-export function dial(dialNumber: string) {
31
-    return (dispatch: Dispatch<*>, getState: Function) => {
32
-        const { conference } = getState()['features/base/conference'];
33
-
34
-        conference.dial(dialNumber);
35
-    };
36
-}
37
-
38
-/**
39
- * Sends an ajax request for dial-out country codes.
40
- *
41
- * @param {string} dialNumber - The dial number to check for validity.
42
- * @returns {Function}
43
- */
44
-export function checkDialNumber(dialNumber: string) {
45
-    return (dispatch: Dispatch<*>, getState: Function) => {
46
-        const { dialOutAuthUrl } = getState()['features/base/config'];
47
-
48
-        if (!dialOutAuthUrl) {
49
-            // no auth url, let's say it is valid
50
-            const response = {};
51
-
52
-            response.allow = true;
53
-            dispatch({
54
-                type: PHONE_NUMBER_CHECKED,
55
-                response
56
-            });
57
-
58
-            return;
59
-        }
60
-
61
-        const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`;
62
-
63
-        $.getJSON(fullUrl)
64
-            .then(response =>
65
-                dispatch({
66
-                    type: PHONE_NUMBER_CHECKED,
67
-                    response
68
-                }))
69
-            .catch(error =>
70
-                dispatch({
71
-                    type: DIAL_OUT_SERVICE_FAILED,
72
-                    error
73
-                }));
74
-    };
75
-}
76
-
77
-/**
78
- * Sends an ajax request for dial-out country codes.
79
- *
80
- * @returns {Function}
81
- */
82
-export function updateDialOutCodes() {
83
-    return (dispatch: Dispatch<*>, getState: Function) => {
84
-        const { dialOutCodesUrl } = getState()['features/base/config'];
85
-
86
-        if (!dialOutCodesUrl) {
87
-            return;
88
-        }
89
-
90
-        $.getJSON(dialOutCodesUrl)
91
-            .then(response =>
92
-                dispatch({
93
-                    type: DIAL_OUT_CODES_UPDATED,
94
-                    response
95
-                }))
96
-            .catch(error =>
97
-                dispatch({
98
-                    type: DIAL_OUT_SERVICE_FAILED,
99
-                    error
100
-                }));
101
-    };
102
-}

+ 0
- 39
react/features/dial-out/components/CountryIcon.js View File

1
-import PropTypes from 'prop-types';
2
-import React, { Component } from 'react';
3
-
4
-/**
5
- * Implements a React {@link Component} to render a country flag icon.
6
- */
7
-export default class CountryIcon extends Component {
8
-    /**
9
-     * {@code CountryIcon}'s property types.
10
-     *
11
-     * @static
12
-     */
13
-    static propTypes = {
14
-        /**
15
-         * The css style class name.
16
-         */
17
-        className: PropTypes.string,
18
-
19
-        /**
20
-         * The 2-letter country code.
21
-         */
22
-        countryCode: PropTypes.string
23
-    };
24
-
25
-    /**
26
-     * Implements React's {@link Component#render()}.
27
-     *
28
-     * @inheritdoc
29
-     * @returns {ReactElement}
30
-     */
31
-    render() {
32
-        const iconClassName
33
-            = `flag-icon flag-icon-${
34
-                this.props.countryCode} flag-icon-squared ${
35
-                this.props.className}`;
36
-
37
-        return <span className = { iconClassName } />;
38
-    }
39
-}

+ 0
- 0
react/features/dial-out/components/DialOutDialog.native.js View File


+ 0
- 248
react/features/dial-out/components/DialOutDialog.web.js View File

1
-import PropTypes from 'prop-types';
2
-import React, { Component } from 'react';
3
-import { connect } from 'react-redux';
4
-
5
-import { translate } from '../../base/i18n';
6
-import { Dialog } from '../../base/dialog';
7
-
8
-import { cancel, checkDialNumber, dial } from '../actions';
9
-import DialOutNumbersForm from './DialOutNumbersForm';
10
-
11
-/**
12
- * Implements a React {@link Component} which allows the user to dial out from
13
- * the conference.
14
- */
15
-class DialOutDialog extends Component {
16
-    /**
17
-     * {@code DialOutDialog} component's property types.
18
-     *
19
-     * @static
20
-     */
21
-    static propTypes = {
22
-        /**
23
-         * The redux state representing the list of dial-out codes.
24
-         */
25
-        _dialOutCodes: PropTypes.array,
26
-
27
-        /**
28
-         * Property indicating if a dial number is allowed.
29
-         */
30
-        _isDialNumberAllowed: PropTypes.bool,
31
-
32
-        /**
33
-         * The function performing the cancel action.
34
-         */
35
-        cancel: PropTypes.func,
36
-
37
-        /**
38
-         * The function performing the phone number validity check.
39
-         */
40
-        checkDialNumber: PropTypes.func,
41
-
42
-        /**
43
-         * The function performing the dial action.
44
-         */
45
-        dial: PropTypes.func,
46
-
47
-        /**
48
-         * Invoked to obtain translated strings.
49
-         */
50
-        t: PropTypes.func
51
-    };
52
-
53
-    /**
54
-     * Initializes a new {@code DialOutNumbersForm} instance.
55
-     *
56
-     * @param {Object} props - The read-only properties with which the new
57
-     * instance is to be initialized.
58
-     */
59
-    constructor(props) {
60
-        super(props);
61
-
62
-        this.state = {
63
-            /**
64
-             * The number to dial.
65
-             */
66
-            dialNumber: '',
67
-
68
-            /**
69
-             * Indicates if the dial input is currently empty.
70
-             */
71
-            isDialInputEmpty: true
72
-        };
73
-
74
-        // Bind event handlers so they are only bound once for every instance.
75
-        this._onDialNumberChange = this._onDialNumberChange.bind(this);
76
-        this._onCancel = this._onCancel.bind(this);
77
-        this._onSubmit = this._onSubmit.bind(this);
78
-    }
79
-
80
-    /**
81
-     * Implements React's {@link Component#render()}.
82
-     *
83
-     * @inheritdoc
84
-     * @returns {ReactElement}
85
-     */
86
-    render() {
87
-        const { _isDialNumberAllowed } = this.props;
88
-
89
-        return (
90
-            <Dialog
91
-                okDisabled = { this.state.isDialInputEmpty
92
-                    || !_isDialNumberAllowed }
93
-                okTitleKey = 'dialOut.dial'
94
-                onCancel = { this._onCancel }
95
-                onSubmit = { this._onSubmit }
96
-                titleKey = 'dialOut.dialOut'
97
-                width = 'small'>
98
-                { this._renderContent() }
99
-            </Dialog>
100
-        );
101
-    }
102
-
103
-    /**
104
-     * Formats the dial number in a way to remove all non digital characters
105
-     * from it (including spaces, brackets, dash, dot, etc.).
106
-     *
107
-     * @param {string} dialNumber - The phone number to format.
108
-     * @private
109
-     * @returns {string} - The formatted phone number.
110
-     */
111
-    _formatDialNumber(dialNumber) {
112
-        return dialNumber.replace(/\D/g, '');
113
-    }
114
-
115
-    /**
116
-     * Renders the dialog content.
117
-     *
118
-     * @returns {ReactElement}
119
-     * @private
120
-     */
121
-    _renderContent() {
122
-        const { _isDialNumberAllowed } = this.props;
123
-
124
-        return (
125
-            <div className = 'dial-out-content'>
126
-                { _isDialNumberAllowed ? '' : this._renderErrorMessage() }
127
-                <DialOutNumbersForm
128
-                    onChange = { this._onDialNumberChange } />
129
-            </div>);
130
-    }
131
-
132
-    /**
133
-     * Renders the error message to display if the dial phone number is not
134
-     * allowed.
135
-     *
136
-     * @returns {ReactElement}
137
-     * @private
138
-     */
139
-    _renderErrorMessage() {
140
-        const { t } = this.props;
141
-
142
-        return (
143
-            <div className = 'dial-out-error'>
144
-                { t('dialOut.phoneNotAllowed') }
145
-            </div>);
146
-    }
147
-
148
-    /**
149
-     * Cancel the dial out.
150
-     *
151
-     * @private
152
-     * @returns {boolean} - Returns true to indicate that the dialog should be
153
-     * closed.
154
-     */
155
-    _onCancel() {
156
-        this.props.cancel();
157
-
158
-        return true;
159
-    }
160
-
161
-    /**
162
-     * Dials the number.
163
-     *
164
-     * @private
165
-     * @returns {boolean} - Returns true to indicate that the dialog should be
166
-     * closed.
167
-     */
168
-    _onSubmit() {
169
-        if (this.props._isDialNumberAllowed) {
170
-            this.props.dial(this.state.dialNumber);
171
-        }
172
-
173
-        return true;
174
-    }
175
-
176
-    /**
177
-     * Updates the dialNumber and check for validity.
178
-     *
179
-     * @param {string} dialCode - The dial code value.
180
-     * @param {string} dialInput - The dial input value.
181
-     * @private
182
-     * @returns {void}
183
-     */
184
-    _onDialNumberChange(dialCode, dialInput) {
185
-        let formattedDialInput, formattedNumber;
186
-
187
-        // if there are no dial out codes it is possible they are disabled
188
-        // so we get the input as is, it can be just a sip address
189
-        if (this.props._dialOutCodes) {
190
-            // We remove all starting zeros from the dial input before attaching
191
-            // it to the country code.
192
-            formattedDialInput = dialInput.replace(/^(0+)/, '');
193
-
194
-            const dialNumber = `${dialCode}${formattedDialInput}`;
195
-
196
-            formattedNumber = this._formatDialNumber(dialNumber);
197
-
198
-            this.props.checkDialNumber(formattedNumber);
199
-        } else {
200
-            formattedNumber = formattedDialInput = dialInput;
201
-        }
202
-
203
-        this.setState({
204
-            dialNumber: formattedNumber,
205
-            isDialInputEmpty: !formattedDialInput
206
-            || formattedDialInput.length === 0
207
-        });
208
-    }
209
-}
210
-
211
-/**
212
- * Maps (parts of) the Redux state to the associated
213
- * {@code DialOutDialog}'s props.
214
- *
215
- * @param {Object} state - The Redux state.
216
- * @private
217
- * @returns {{
218
- *     _isDialNumberAllowed: boolean
219
- * }}
220
- */
221
-function _mapStateToProps(state) {
222
-    const { dialOutCodes, isDialNumberAllowed } = state['features/dial-out'];
223
-
224
-    return {
225
-        /**
226
-         * List of dial-out codes.
227
-         *
228
-         * @private
229
-         * @type {array}
230
-         */
231
-        _dialOutCodes: dialOutCodes,
232
-
233
-        /**
234
-         * Property indicating if a dial number is allowed.
235
-         *
236
-         * @private
237
-         * @type {boolean}
238
-         */
239
-        _isDialNumberAllowed: isDialNumberAllowed
240
-    };
241
-}
242
-
243
-export default translate(
244
-    connect(_mapStateToProps, {
245
-        cancel,
246
-        checkDialNumber,
247
-        dial
248
-    })(DialOutDialog));

+ 0
- 369
react/features/dial-out/components/DialOutNumbersForm.web.js View File

1
-import { DropdownMenuStateless as DropdownMenu } from '@atlaskit/dropdown-menu';
2
-import { FieldTextStateless as TextField } from '@atlaskit/field-text';
3
-import ChevronDownIcon from '@atlaskit/icon/glyph/chevron-down';
4
-import PropTypes from 'prop-types';
5
-import React, { Component } from 'react';
6
-import { connect } from 'react-redux';
7
-
8
-import { translate } from '../../base/i18n';
9
-
10
-import { updateDialOutCodes } from '../actions';
11
-import CountryIcon from './CountryIcon';
12
-
13
-/**
14
- * The default value of the country if the fetch service is unavailable.
15
- *
16
- * @type {{
17
- *     code: string,
18
- *     dialCode: string,
19
- *     name: string
20
- * }}
21
- */
22
-const DEFAULT_COUNTRY = {
23
-    code: 'US',
24
-    dialCode: '+1',
25
-    name: 'United States'
26
-};
27
-
28
-/**
29
- * React {@code Component} responsible for fetching and displaying dial-out
30
- * country codes, as well as dialing a phone number.
31
- *
32
- * @extends Component
33
- */
34
-class DialOutNumbersForm extends Component {
35
-    /**
36
-     * {@code DialOutNumbersForm}'s property types.
37
-     *
38
-     * @static
39
-     */
40
-    static propTypes = {
41
-        /**
42
-         * The redux state representing the list of dial-out codes.
43
-         */
44
-        _dialOutCodes: PropTypes.array,
45
-
46
-        /**
47
-         * The function called on every dial input change.
48
-         */
49
-        onChange: PropTypes.func,
50
-
51
-        /**
52
-         * Invoked to obtain translated strings.
53
-         */
54
-        t: PropTypes.func,
55
-
56
-        /**
57
-         * Invoked to send an ajax request for dial-out codes.
58
-         */
59
-        updateDialOutCodes: PropTypes.func
60
-    };
61
-
62
-    /**
63
-     * Initializes a new {@code DialOutNumbersForm} instance.
64
-     *
65
-     * @param {Object} props - The read-only properties with which the new
66
-     * instance is to be initialized.
67
-     */
68
-    constructor(props) {
69
-        super(props);
70
-
71
-        this.state = {
72
-            dialInput: '',
73
-
74
-            /**
75
-             * Whether or not the dropdown should be open.
76
-             *
77
-             * @type {boolean}
78
-             */
79
-            isDropdownOpen: false,
80
-
81
-            /**
82
-             * The selected country.
83
-             *
84
-             * @type {Object}
85
-             */
86
-            selectedCountry: DEFAULT_COUNTRY
87
-        };
88
-
89
-        /**
90
-         * The internal reference to the DOM/HTML element backing the React
91
-         * {@code Component} text input.
92
-         *
93
-         * @private
94
-         * @type {HTMLInputElement}
95
-         */
96
-        this._dialInputElem = null;
97
-
98
-        // Bind event handlers so they are only bound once for every instance.
99
-        this._onDropdownTriggerInputChange
100
-            = this._onDropdownTriggerInputChange.bind(this);
101
-        this._onInputChange = this._onInputChange.bind(this);
102
-        this._onOpenChange = this._onOpenChange.bind(this);
103
-        this._onSelect = this._onSelect.bind(this);
104
-        this._setDialInputElement = this._setDialInputElement.bind(this);
105
-    }
106
-
107
-    /**
108
-     * Dispatches a request for dial out codes if not already present in the
109
-     * redux store. If dial out codes are present, sets a default code to
110
-     * display in the dropdown trigger.
111
-     *
112
-     * @inheritdoc
113
-     * @returns {void}
114
-     */
115
-    componentDidMount() {
116
-        const dialOutCodes = this.props._dialOutCodes;
117
-
118
-        if (dialOutCodes) {
119
-            this._setDefaultCode(dialOutCodes);
120
-        } else {
121
-            this.props.updateDialOutCodes();
122
-        }
123
-    }
124
-
125
-    /**
126
-     * Monitors for dial out code updates and sets a default code to display in
127
-     * the dropdown trigger if not already set.
128
-     *
129
-     * @inheritdoc
130
-     * @returns {void}
131
-     */
132
-    componentWillReceiveProps(nextProps) {
133
-        if (!this.state.selectedCountry && nextProps._dialOutCodes) {
134
-            this._setDefaultCode(nextProps._dialOutCodes);
135
-        }
136
-    }
137
-
138
-    /**
139
-     * Implements React's {@link Component#render()}.
140
-     *
141
-     * @inheritdoc
142
-     * @returns {ReactElement}
143
-     */
144
-    render() {
145
-        const { t, _dialOutCodes } = this.props;
146
-
147
-        return (
148
-            <div className = 'form-control'>
149
-                { _dialOutCodes ? this._createDropdownMenu(
150
-                        this._formatCountryCodes(_dialOutCodes)) : null }
151
-                <div className = 'dial-out-input'>
152
-                    <TextField
153
-                        autoFocus = { true }
154
-                        isLabelHidden = { true }
155
-                        label = { 'dial-out-input-field' }
156
-                        onChange = { this._onInputChange }
157
-                        placeholder = { t('dialOut.enterPhone') }
158
-                        ref = { this._setDialInputElement }
159
-                        shouldFitContainer = { true }
160
-                        value = { this.state.dialInput } />
161
-                </div>
162
-            </div>
163
-        );
164
-    }
165
-
166
-    /**
167
-     * Creates a {@code DropdownMenu} instance.
168
-     *
169
-     * @param {Array} items - The content to display within the dropdown.
170
-     * @returns {ReactElement}
171
-     */
172
-    _createDropdownMenu(items) {
173
-        const { code, dialCode } = this.state.selectedCountry;
174
-
175
-        return (
176
-            <div className = 'dropdown-container'>
177
-                <DropdownMenu
178
-                    isOpen = { this.state.isDropdownOpen }
179
-                    items = { [ { items } ] }
180
-                    onItemActivated = { this._onSelect }
181
-                    onOpenChange = { this._onOpenChange }
182
-                    shouldFitContainer = { false }>
183
-                    { this._createDropdownTrigger(dialCode, code) }
184
-                </DropdownMenu>
185
-            </div>
186
-        );
187
-    }
188
-
189
-    /**
190
-     * Creates a React {@code Component} with a readonly HTMLInputElement as a
191
-     * trigger for displaying the dropdown menu. The {@code Component} will also
192
-     * display the currently selected number.
193
-     *
194
-     * @param {string} dialCode - The +xx dial code.
195
-     * @param {string} countryCode - The country 2 letter code.
196
-     * @private
197
-     * @returns {ReactElement}
198
-     */
199
-    _createDropdownTrigger(dialCode, countryCode) {
200
-        return (
201
-            <div className = 'dropdown'>
202
-                <CountryIcon
203
-                    className = 'dial-out-flag-icon'
204
-                    countryCode = { `${countryCode}` } />
205
-                { /**
206
-                   * FIXME Replace TextField with AtlasKit Button when an issue
207
-                   * with icons shrinking due to button text is fixed.
208
-                   */ }
209
-                <TextField
210
-                    className = 'input-control dial-out-code'
211
-                    isLabelHidden = { true }
212
-                    isReadOnly = { true }
213
-                    label = 'dial-out-code'
214
-                    onChange = { this._onDropdownTriggerInputChange }
215
-                    type = 'text'
216
-                    value = { dialCode || '' } />
217
-                <span className = 'dropdown-trigger-icon'>
218
-                    <ChevronDownIcon
219
-                        label = 'expand'
220
-                        size = 'small' />
221
-                </span>
222
-            </div>
223
-        );
224
-    }
225
-
226
-    /**
227
-     * Transforms the passed in numbers object into an array of objects that can
228
-     * be parsed by {@code DropdownMenu}.
229
-     *
230
-     * @param {Object} countryCodes - The list of country codes.
231
-     * @private
232
-     * @returns {Array<Object>}
233
-     */
234
-    _formatCountryCodes(countryCodes) {
235
-        return countryCodes.map(country => {
236
-            const countryIcon
237
-                = <CountryIcon countryCode = { `${country.code}` } />;
238
-            const countryElement
239
-                = <span>{countryIcon} { country.name }</span>;
240
-
241
-            return {
242
-                content: `${country.dialCode}`,
243
-                country,
244
-                elemBefore: countryElement
245
-            };
246
-        });
247
-    }
248
-
249
-    /**
250
-     * Updates the dialNumber when changes to the dial text or code happen.
251
-     *
252
-     * @private
253
-     * @returns {void}
254
-     */
255
-    _onDialNumberChange() {
256
-        const { dialCode } = this.state.selectedCountry;
257
-
258
-        this.props.onChange(dialCode, this.state.dialInput);
259
-    }
260
-
261
-    /**
262
-     * This is a no-op function used to stub out TextField's onChange in order
263
-     * to prevent TextField from printing prop type validation errors. TextField
264
-     * is used as a trigger for the dropdown in {@code DialOutNumbersForm} to
265
-     * get the desired AtlasKit input look for the UI.
266
-     *
267
-     * @returns {void}
268
-     */
269
-    _onDropdownTriggerInputChange() {
270
-        // Intentionally left empty.
271
-    }
272
-
273
-    /**
274
-     * Updates the dialInput state when the input changes.
275
-     *
276
-     * @param {Object} e - The event notifying us of the change.
277
-     * @private
278
-     * @returns {void}
279
-     */
280
-    _onInputChange(e) {
281
-        this.setState({
282
-            dialInput: e.target.value
283
-        }, () => {
284
-            this._onDialNumberChange();
285
-        });
286
-    }
287
-
288
-    /**
289
-     * Sets the internal state to either open or close the dropdown. If the
290
-     * dropdown is disabled, the state will always be set to false.
291
-     *
292
-     * @param {Object} dropdownEvent - The even returned from clicking on the
293
-     * dropdown trigger.
294
-     * @private
295
-     * @returns {void}
296
-     */
297
-    _onOpenChange(dropdownEvent) {
298
-        this.setState({
299
-            isDropdownOpen: dropdownEvent.isOpen
300
-        });
301
-    }
302
-
303
-    /**
304
-     * Updates the internal state of the currently selected country code.
305
-     *
306
-     * @param {Object} selection - Event from choosing an dropdown option.
307
-     * @private
308
-     * @returns {void}
309
-     */
310
-    _onSelect(selection) {
311
-        this.setState({
312
-            isDropdownOpen: false,
313
-            selectedCountry: selection.item.country
314
-        }, () => {
315
-            this._onDialNumberChange();
316
-
317
-            this._dialInputElem.focus();
318
-        });
319
-    }
320
-
321
-    /**
322
-     * Updates the internal state of the currently selected number by defaulting
323
-     * to the first available number.
324
-     *
325
-     * @param {Object} countryCodes - The list of country codes to choose from
326
-     * for setting a default code.
327
-     * @private
328
-     * @returns {void}
329
-     */
330
-    _setDefaultCode(countryCodes) {
331
-        this.setState({
332
-            selectedCountry: countryCodes[0]
333
-        });
334
-    }
335
-
336
-    /**
337
-     * Sets the internal reference to the DOM/HTML element backing the React
338
-     * {@code Component} dial input.
339
-     *
340
-     * @param {HTMLInputElement} input - The DOM/HTML element for this
341
-     * {@code Component}'s text input.
342
-     * @private
343
-     * @returns {void}
344
-     */
345
-    _setDialInputElement(input) {
346
-        this._dialInputElem = input;
347
-    }
348
-}
349
-
350
-/**
351
- * Maps (parts of) the Redux state to the associated
352
- * {@code DialOutNumbersForm}'s props.
353
- *
354
- * @param {Object} state - The Redux state.
355
- * @private
356
- * @returns {{
357
- *     _dialOutCodes: Object
358
- * }}
359
- */
360
-function _mapStateToProps(state) {
361
-    const { dialOutCodes } = state['features/dial-out'];
362
-
363
-    return {
364
-        _dialOutCodes: dialOutCodes
365
-    };
366
-}
367
-
368
-export default translate(
369
-    connect(_mapStateToProps, { updateDialOutCodes })(DialOutNumbersForm));

+ 0
- 1
react/features/dial-out/components/index.js View File

1
-export { default as DialOutDialog } from './DialOutDialog';

+ 0
- 4
react/features/dial-out/index.js View File

1
-export * from './actions';
2
-export * from './components';
3
-
4
-import './reducer';

+ 0
- 53
react/features/dial-out/reducer.js View File

1
-import {
2
-    ReducerRegistry
3
-} from '../base/redux';
4
-
5
-import {
6
-    DIAL_OUT_CANCELED,
7
-    DIAL_OUT_CODES_UPDATED,
8
-    DIAL_OUT_SERVICE_FAILED,
9
-    PHONE_NUMBER_CHECKED
10
-} from './actionTypes';
11
-
12
-const DEFAULT_STATE = {
13
-    dialOutCodes: null,
14
-    error: null,
15
-    isDialNumberAllowed: true
16
-};
17
-
18
-ReducerRegistry.register(
19
-    'features/dial-out',
20
-    (state = DEFAULT_STATE, action) => {
21
-        switch (action.type) {
22
-        case DIAL_OUT_CANCELED: {
23
-            // if we have already downloaded codes fill them in default state
24
-            // to skip another ajax query
25
-            return {
26
-                ...DEFAULT_STATE,
27
-                dialOutCodes: state.dialOutCodes
28
-            };
29
-        }
30
-        case DIAL_OUT_CODES_UPDATED: {
31
-            return {
32
-                ...state,
33
-                error: null,
34
-                dialOutCodes: action.response
35
-            };
36
-        }
37
-        case DIAL_OUT_SERVICE_FAILED: {
38
-            return {
39
-                ...state,
40
-                error: action.error
41
-            };
42
-        }
43
-        case PHONE_NUMBER_CHECKED: {
44
-            return {
45
-                ...state,
46
-                error: null,
47
-                isDialNumberAllowed: action.response.allow
48
-            };
49
-        }
50
-        }
51
-
52
-        return state;
53
-    });

+ 51
- 9
react/features/filmstrip/components/Filmstrip.web.js View File

5
 import React, { Component } from 'react';
5
 import React, { Component } from 'react';
6
 import { connect } from 'react-redux';
6
 import { connect } from 'react-redux';
7
 
7
 
8
+import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
8
 import { InviteButton } from '../../invite';
9
 import { InviteButton } from '../../invite';
9
 import { Toolbox } from '../../toolbox';
10
 import { Toolbox } from '../../toolbox';
10
 
11
 
43
          */
44
          */
44
         _hovered: PropTypes.bool,
45
         _hovered: PropTypes.bool,
45
 
46
 
47
+        /**
48
+         * Whether or not the feature to directly invite people into the
49
+         * conference is available.
50
+         */
51
+        _isAddToCallAvailable: PropTypes.bool,
52
+
53
+        /**
54
+         * Whether or not the feature to dial out to number to join the
55
+         * conference is available.
56
+         */
57
+        _isDialOutAvailable: PropTypes.bool,
58
+
46
         /**
59
         /**
47
          * Whether or not the remote videos should be visible. Will toggle
60
          * Whether or not the remote videos should be visible. Will toggle
48
          * a class for hiding the videos.
61
          * a class for hiding the videos.
93
      * @returns {ReactElement}
106
      * @returns {ReactElement}
94
      */
107
      */
95
     render() {
108
     render() {
109
+        const {
110
+            _hideInviteButton,
111
+            _isAddToCallAvailable,
112
+            _isDialOutAvailable,
113
+            _remoteVideosVisible,
114
+            filmstripOnly
115
+        } = this.props;
116
+
96
         /**
117
         /**
97
          * Note: Appending of {@code RemoteVideo} views is handled through
118
          * Note: Appending of {@code RemoteVideo} views is handled through
98
          * VideoLayout. The views do not get blown away on render() because
119
          * VideoLayout. The views do not get blown away on render() because
102
          * modified, then the views will get blown away.
123
          * modified, then the views will get blown away.
103
          */
124
          */
104
 
125
 
105
-        const filmstripClassNames = `filmstrip ${this.props._remoteVideosVisible
106
-            ? '' : 'hide-videos'}`;
126
+        const filmstripClassNames = `filmstrip ${_remoteVideosVisible ? ''
127
+            : 'hide-videos'}`;
107
 
128
 
108
         return (
129
         return (
109
             <div className = { filmstripClassNames }>
130
             <div className = { filmstripClassNames }>
110
-                { this.props.filmstripOnly ? <Toolbox /> : null }
131
+                { filmstripOnly ? <Toolbox /> : null }
111
                 <div
132
                 <div
112
                     className = 'filmstrip__videos'
133
                     className = 'filmstrip__videos'
113
                     id = 'remoteVideos'>
134
                     id = 'remoteVideos'>
116
                         id = 'filmstripLocalVideo'
137
                         id = 'filmstripLocalVideo'
117
                         onMouseOut = { this._onMouseOut }
138
                         onMouseOut = { this._onMouseOut }
118
                         onMouseOver = { this._onMouseOver }>
139
                         onMouseOver = { this._onMouseOver }>
119
-                        { this.props.filmstripOnly
120
-                            || this.props._hideInviteButton
121
-                            ? null : <InviteButton /> }
140
+                        { filmstripOnly || _hideInviteButton
141
+                            ? null
142
+                            : <InviteButton
143
+                                enableAddPeople = { _isAddToCallAvailable }
144
+                                enableDialOut = { _isDialOutAvailable } /> }
122
                         <div id = 'filmstripLocalVideoThumbnail' />
145
                         <div id = 'filmstripLocalVideoThumbnail' />
123
                     </div>
146
                     </div>
124
                     <div
147
                     <div
192
  * @param {Object} state - The Redux state.
215
  * @param {Object} state - The Redux state.
193
  * @private
216
  * @private
194
  * @returns {{
217
  * @returns {{
195
- *     _hovered: boolean,
196
  *     _hideInviteButton: boolean,
218
  *     _hideInviteButton: boolean,
219
+ *     _hovered: boolean,
220
+ *     _isAddToCallAvailable: boolean,
221
+ *     _isDialOutAvailable: boolean,
197
  *     _remoteVideosVisible: boolean
222
  *     _remoteVideosVisible: boolean
198
  * }}
223
  * }}
199
  */
224
  */
200
 function _mapStateToProps(state) {
225
 function _mapStateToProps(state) {
226
+    const { conference } = state['features/base/conference'];
227
+    const {
228
+        enableUserRolesBasedOnToken,
229
+        iAmRecorder
230
+    } = state['features/base/config'];
231
+    const { isGuest } = state['features/base/jwt'];
232
+    const { hovered } = state['features/filmstrip'];
233
+
234
+    const isAddToCallAvailable = !isGuest;
235
+    const isDialOutAvailable
236
+        = getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR
237
+                && conference && conference.isSIPCallingSupported()
238
+                && (!enableUserRolesBasedOnToken || !isGuest);
239
+
201
     return {
240
     return {
202
-        _hovered: state['features/filmstrip'].hovered,
203
-        _hideInviteButton: state['features/base/config'].iAmRecorder,
241
+        _hideInviteButton: iAmRecorder
242
+            || (!isAddToCallAvailable && !isDialOutAvailable),
243
+        _hovered: hovered,
244
+        _isAddToCallAvailable: isAddToCallAvailable,
245
+        _isDialOutAvailable: isDialOutAvailable,
204
         _remoteVideosVisible: shouldRemoteVideosBeVisible(state)
246
         _remoteVideosVisible: shouldRemoteVideosBeVisible(state)
205
     };
247
     };
206
 }
248
 }

+ 395
- 74
react/features/invite/components/AddPeopleDialog.web.js View File

11
 import { Dialog, hideDialog } from '../../base/dialog';
11
 import { Dialog, hideDialog } from '../../base/dialog';
12
 import { translate } from '../../base/i18n';
12
 import { translate } from '../../base/i18n';
13
 import { MultiSelectAutocomplete } from '../../base/react';
13
 import { MultiSelectAutocomplete } from '../../base/react';
14
-
15
-import { invitePeopleAndChatRooms, searchDirectory } from '../functions';
16
 import { inviteVideoRooms } from '../../videosipgw';
14
 import { inviteVideoRooms } from '../../videosipgw';
17
 
15
 
16
+import {
17
+    checkDialNumber,
18
+    invitePeopleAndChatRooms,
19
+    searchDirectory
20
+} from '../functions';
21
+
22
+const logger = require('jitsi-meet-logger').getLogger(__filename);
23
+
18
 declare var interfaceConfig: Object;
24
 declare var interfaceConfig: Object;
19
 
25
 
26
+const isPhoneNumberRegex
27
+    = new RegExp(interfaceConfig.PHONE_NUMBER_REGEX || '^[0-9+()-\\s]*$');
28
+
20
 /**
29
 /**
21
  * The dialog that allows to invite people to the call.
30
  * The dialog that allows to invite people to the call.
22
  */
31
  */
33
          */
42
          */
34
         _conference: PropTypes.object,
43
         _conference: PropTypes.object,
35
 
44
 
45
+        /**
46
+         * The URL for validating if a phone number can be called.
47
+         */
48
+        _dialOutAuthUrl: PropTypes.string,
49
+
36
         /**
50
         /**
37
          * The URL pointing to the service allowing for people invite.
51
          * The URL pointing to the service allowing for people invite.
38
          */
52
          */
58
          */
72
          */
59
         _peopleSearchUrl: PropTypes.string,
73
         _peopleSearchUrl: PropTypes.string,
60
 
74
 
75
+        /**
76
+         * Whether or not to show Add People functionality.
77
+         */
78
+        enableAddPeople: PropTypes.bool,
79
+
80
+        /**
81
+         * Whether or not to show Dial Out functionality.
82
+         */
83
+        enableDialOut: PropTypes.bool,
84
+
61
         /**
85
         /**
62
          * The function closing the dialog.
86
          * The function closing the dialog.
63
          */
87
          */
76
 
100
 
77
     _multiselect = null;
101
     _multiselect = null;
78
 
102
 
79
-    _resourceClient = {
80
-        makeQuery: text => {
81
-            const {
82
-                _jwt,
83
-                _peopleSearchQueryTypes,
84
-                _peopleSearchUrl
85
-            } = this.props; // eslint-disable-line no-invalid-this
86
-
87
-            return (
88
-                searchDirectory(
89
-                    _peopleSearchUrl,
90
-                    _jwt,
91
-                    text,
92
-                    _peopleSearchQueryTypes));
93
-        },
94
-
95
-        parseResults: response => response.map(user => {
96
-            return {
97
-                content: user.name,
98
-                elemBefore: <Avatar
99
-                    size = 'medium'
100
-                    src = { user.avatar } />,
101
-                item: user,
102
-                value: user.id
103
-            };
104
-        })
105
-    };
103
+    _resourceClient: Object;
106
 
104
 
107
     state = {
105
     state = {
108
         /**
106
         /**
116
          */
114
          */
117
         addToCallInProgress: false,
115
         addToCallInProgress: false,
118
 
116
 
117
+
118
+        // FIXME: Remove usage of Immutable. {@code MultiSelectAutocomplete}
119
+        // will default to having its internal implementation use a plain array
120
+        // if no {@link defaultValue} is passed in. As such is the case, this
121
+        // instance of Immutable.List gets overridden with an array on the first
122
+        // search.
119
         /**
123
         /**
120
          * The list of invite items.
124
          * The list of invite items.
121
          */
125
          */
133
 
137
 
134
         // Bind event handlers so they are only bound once per instance.
138
         // Bind event handlers so they are only bound once per instance.
135
         this._isAddDisabled = this._isAddDisabled.bind(this);
139
         this._isAddDisabled = this._isAddDisabled.bind(this);
140
+        this._onItemSelected = this._onItemSelected.bind(this);
136
         this._onSelectionChange = this._onSelectionChange.bind(this);
141
         this._onSelectionChange = this._onSelectionChange.bind(this);
137
         this._onSubmit = this._onSubmit.bind(this);
142
         this._onSubmit = this._onSubmit.bind(this);
143
+        this._parseQueryResults = this._parseQueryResults.bind(this);
144
+        this._query = this._query.bind(this);
138
         this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
145
         this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
146
+
147
+        this._resourceClient = {
148
+            makeQuery: this._query,
149
+            parseResults: this._parseQueryResults
150
+        };
139
     }
151
     }
140
 
152
 
141
     /**
153
     /**
153
                 && !this.state.addToCallInProgress
165
                 && !this.state.addToCallInProgress
154
                 && !this.state.addToCallError
166
                 && !this.state.addToCallError
155
                 && this._multiselect) {
167
                 && this._multiselect) {
156
-            this._multiselect.clear();
168
+            this._multiselect.setSelectedItems([]);
157
         }
169
         }
158
     }
170
     }
159
 
171
 
163
      * @returns {ReactElement}
175
      * @returns {ReactElement}
164
      */
176
      */
165
     render() {
177
     render() {
178
+        const { enableAddPeople, enableDialOut, t } = this.props;
179
+        let isMultiSelectDisabled = this.state.addToCallInProgress || false;
180
+        let placeholder;
181
+        let loadingMessage;
182
+        let noMatches;
183
+
184
+        if (enableAddPeople && enableDialOut) {
185
+            loadingMessage = 'addPeople.loading';
186
+            noMatches = 'addPeople.noResults';
187
+            placeholder = 'addPeople.searchPeopleAndNumbers';
188
+        } else if (enableAddPeople) {
189
+            loadingMessage = 'addPeople.loadingPeople';
190
+            noMatches = 'addPeople.noResults';
191
+            placeholder = 'addPeople.searchPeople';
192
+        } else if (enableDialOut) {
193
+            loadingMessage = 'addPeople.loadingNumber';
194
+            noMatches = 'addPeople.noValidNumbers';
195
+            placeholder = 'addPeople.searchNumbers';
196
+        } else {
197
+            isMultiSelectDisabled = true;
198
+            noMatches = 'addPeople.noResults';
199
+            placeholder = 'addPeople.disabled';
200
+        }
201
+
166
         return (
202
         return (
167
             <Dialog
203
             <Dialog
168
                 okDisabled = { this._isAddDisabled() }
204
                 okDisabled = { this._isAddDisabled() }
169
                 okTitleKey = 'addPeople.add'
205
                 okTitleKey = 'addPeople.add'
170
                 onSubmit = { this._onSubmit }
206
                 onSubmit = { this._onSubmit }
171
                 titleKey = 'addPeople.title'
207
                 titleKey = 'addPeople.title'
172
-                width = 'small'>
173
-                { this._renderUserInputForm() }
208
+                width = 'medium'>
209
+                <div className = 'add-people-form-wrap'>
210
+                    { this._renderErrorMessage() }
211
+                    <MultiSelectAutocomplete
212
+                        isDisabled = { isMultiSelectDisabled }
213
+                        loadingMessage = { t(loadingMessage) }
214
+                        noMatchesFound = { t(noMatches) }
215
+                        onItemSelected = { this._onItemSelected }
216
+                        onSelectionChange = { this._onSelectionChange }
217
+                        placeholder = { t(placeholder) }
218
+                        ref = { this._setMultiSelectElement }
219
+                        resourceClient = { this._resourceClient }
220
+                        shouldFitContainer = { true }
221
+                        shouldFocus = { true } />
222
+                </div>
174
             </Dialog>
223
             </Dialog>
175
         );
224
         );
176
     }
225
     }
177
 
226
 
227
+    _getDigitsOnly: (string) => string;
228
+
229
+    /**
230
+     * Removes all non-numeric characters from a string.
231
+     *
232
+     * @param {string} text - The string from which to remove all characters
233
+     * except numbers.
234
+     * @private
235
+     * @returns {string} A string with only numbers.
236
+     */
237
+    _getDigitsOnly(text = '') {
238
+        return text.replace(/\D/g, '');
239
+    }
240
+
178
     _isAddDisabled: () => boolean;
241
     _isAddDisabled: () => boolean;
179
 
242
 
180
     /**
243
     /**
189
             || this.state.addToCallInProgress;
252
             || this.state.addToCallInProgress;
190
     }
253
     }
191
 
254
 
255
+    _isMaybeAPhoneNumber: (string) => boolean;
256
+
257
+    /**
258
+     * Checks whether a string looks like it could be for a phone number.
259
+     *
260
+     * @param {string} text - The text to check whether or not it could be a
261
+     * phone number.
262
+     * @private
263
+     * @returns {boolean} True if the string looks like it could be a phone
264
+     * number.
265
+     */
266
+    _isMaybeAPhoneNumber(text) {
267
+        if (!isPhoneNumberRegex.test(text)) {
268
+            return false;
269
+        }
270
+
271
+        const digits = this._getDigitsOnly(text);
272
+
273
+        return Boolean(digits.length);
274
+    }
275
+
276
+    _onItemSelected: (Object) => Object;
277
+
278
+    /**
279
+     * Callback invoked when a selection has been made but before it has been
280
+     * set as selected.
281
+     *
282
+     * @param {Object} item - The item that has just been selected.
283
+     * @private
284
+     * @returns {Object} The item to display as selected in the input.
285
+     */
286
+    _onItemSelected(item) {
287
+        if (item.item.type === 'phone') {
288
+            item.content = item.item.number;
289
+        }
290
+
291
+        return item;
292
+    }
293
+
192
     _onSelectionChange: (Map<*, *>) => void;
294
     _onSelectionChange: (Map<*, *>) => void;
193
 
295
 
194
     /**
296
     /**
199
      * @returns {void}
301
      * @returns {void}
200
      */
302
      */
201
     _onSelectionChange(selectedItems) {
303
     _onSelectionChange(selectedItems) {
202
-        const selectedIds = selectedItems.map(o => o.item);
203
-
204
         this.setState({
304
         this.setState({
205
-            inviteItems: selectedIds
305
+            inviteItems: selectedItems
206
         });
306
         });
207
     }
307
     }
208
 
308
 
209
     _onSubmit: () => void;
309
     _onSubmit: () => void;
210
 
310
 
211
     /**
311
     /**
212
-     * Handles the submit button action.
312
+     * Invite people and numbers to the conference. The logic works by inviting
313
+     * numbers, people/rooms, and videosipgw in parallel. All invitees are
314
+     * stored in an array. As each invite succeeds, the invitee is removed
315
+     * from the array. After all invites finish, close the modal if there are
316
+     * no invites left to send. If any are left, that means an invite failed
317
+     * and an error state should display.
213
      *
318
      *
214
      * @private
319
      * @private
215
      * @returns {void}
320
      * @returns {void}
216
      */
321
      */
217
     _onSubmit() {
322
     _onSubmit() {
218
-        if (!this._isAddDisabled()) {
219
-            this.setState({
220
-                addToCallInProgress: true
323
+        if (this._isAddDisabled()) {
324
+            return;
325
+        }
326
+
327
+        this.setState({
328
+            addToCallInProgress: true
329
+        });
330
+
331
+        let allInvitePromises = [];
332
+        let invitesLeftToSend = [
333
+            ...this.state.inviteItems
334
+        ];
335
+
336
+        // First create all promises for dialing out.
337
+        if (this.props.enableDialOut && this.props._conference) {
338
+            const phoneNumbers = invitesLeftToSend.filter(
339
+                ({ item }) => item.type === 'phone');
340
+
341
+            // For each number, dial out. On success, remove the number from
342
+            // {@link invitesLeftToSend}.
343
+            const phoneInvitePromises = phoneNumbers.map(number => {
344
+                const numberToInvite = this._getDigitsOnly(number.item.number);
345
+
346
+                return this.props._conference.dial(numberToInvite)
347
+                        .then(() => {
348
+                            invitesLeftToSend
349
+                                = invitesLeftToSend.filter(invite =>
350
+                                    invite !== number);
351
+                        })
352
+                        .catch(error => logger.error(
353
+                            'Error inviting phone number:', error));
354
+
221
             });
355
             });
222
 
356
 
223
-            const vrooms = this.state.inviteItems.filter(
224
-                i => i.type === 'videosipgw');
357
+            allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
358
+        }
359
+
360
+        if (this.props.enableAddPeople) {
361
+            const usersAndRooms = invitesLeftToSend.filter(i =>
362
+                i.item.type === 'user' || i.item.type === 'room')
363
+                .map(i => i.item);
364
+
365
+            if (usersAndRooms.length) {
366
+                // Send a request to invite all the rooms and users. On success,
367
+                // filter all rooms and users from {@link invitesLeftToSend}.
368
+                const peopleInvitePromise = invitePeopleAndChatRooms(
369
+                    this.props._inviteServiceUrl,
370
+                    this.props._inviteUrl,
371
+                    this.props._jwt,
372
+                    usersAndRooms)
373
+                    .then(() => {
374
+                        invitesLeftToSend = invitesLeftToSend.filter(i =>
375
+                            i.item.type !== 'user' && i.item.type !== 'room');
376
+                    })
377
+                    .catch(error => logger.error(
378
+                        'Error inviting people:', error));
379
+
380
+                allInvitePromises.push(peopleInvitePromise);
381
+            }
382
+
383
+            // Sipgw calls are fire and forget. Invite them to the conference
384
+            // then immediately remove them from {@link invitesLeftToSend}.
385
+            const vrooms = invitesLeftToSend.filter(i =>
386
+                i.item.type === 'videosipgw')
387
+                .map(i => i.item);
225
 
388
 
226
             this.props._conference
389
             this.props._conference
227
                 && vrooms.length > 0
390
                 && vrooms.length > 0
228
-                && this.props.inviteVideoRooms(this.props._conference, vrooms);
229
-
230
-            invitePeopleAndChatRooms(
231
-                this.props._inviteServiceUrl,
232
-                this.props._inviteUrl,
233
-                this.props._jwt,
234
-                this.state.inviteItems.filter(
235
-                    i => i.type === 'user' || i.type === 'room'))
236
-            .then(
237
-                /* onFulfilled */ () => {
238
-                    this.setState({
239
-                        addToCallInProgress: false
240
-                    });
391
+                && this.props.inviteVideoRooms(
392
+                    this.props._conference, vrooms);
393
+
394
+            invitesLeftToSend = invitesLeftToSend.filter(i =>
395
+                i.item.type !== 'videosipgw');
396
+        }
397
+
398
+        Promise.all(allInvitePromises)
399
+            .then(() => {
400
+                // If any invites are left that means something failed to send
401
+                // so treat it as an error.
402
+                if (invitesLeftToSend.length) {
403
+                    logger.error(`${invitesLeftToSend.length} invites failed`);
241
 
404
 
242
-                    this.props.hideDialog();
243
-                },
244
-                /* onRejected */ () => {
245
                     this.setState({
405
                     this.setState({
246
                         addToCallInProgress: false,
406
                         addToCallInProgress: false,
247
                         addToCallError: true
407
                         addToCallError: true
248
                     });
408
                     });
409
+
410
+                    if (this._multiselect) {
411
+                        this._multiselect.setSelectedItems(invitesLeftToSend);
412
+                    }
413
+
414
+                    return;
415
+                }
416
+
417
+                this.setState({
418
+                    addToCallInProgress: false
249
                 });
419
                 });
420
+
421
+                this.props.hideDialog();
422
+            });
423
+    }
424
+
425
+    _parseQueryResults: (Array<Object>, string) => Array<Object>;
426
+
427
+    /**
428
+     * Processes results from requesting available numbers and people by munging
429
+     * each result into a format {@code MultiSelectAutocomplete} can use for
430
+     * display.
431
+     *
432
+     * @param {Array} response - The response object from the server for the
433
+     * query.
434
+     * @private
435
+     * @returns {Object[]} Configuration objects for items to display in the
436
+     * search autocomplete.
437
+     */
438
+    _parseQueryResults(response = []) {
439
+        const { t } = this.props;
440
+        const users = response.filter(item => item.type !== 'phone');
441
+        const userDisplayItems = users.map(user => {
442
+            return {
443
+                content: user.name,
444
+                elemBefore: <Avatar
445
+                    size = 'medium'
446
+                    src = { user.avatar } />,
447
+                item: user,
448
+                tag: {
449
+                    elemBefore: <Avatar
450
+                        size = 'xsmall'
451
+                        src = { user.avatar } />
452
+                },
453
+                value: user.id
454
+            };
455
+        });
456
+
457
+        const numbers = response.filter(item => item.type === 'phone');
458
+        const telephoneIcon = this._renderTelephoneIcon();
459
+
460
+        const numberDisplayItems = numbers.map(number => {
461
+            const numberNotAllowedMessage
462
+                = number.allowed ? '' : t('addPeople.countryNotSupported');
463
+            const countryCodeReminder = number.showCountryCodeReminder
464
+                ? t('addPeople.countryReminder') : '';
465
+            const description
466
+                = `${numberNotAllowedMessage} ${countryCodeReminder}`.trim();
467
+
468
+            return {
469
+                filterValues: [
470
+                    number.originalEntry,
471
+                    number.number
472
+                ],
473
+                content: t('addPeople.telephone', { number: number.number }),
474
+                description,
475
+                isDisabled: !number.allowed,
476
+                elemBefore: telephoneIcon,
477
+                item: number,
478
+                tag: {
479
+                    elemBefore: telephoneIcon
480
+                },
481
+                value: number.number
482
+            };
483
+        });
484
+
485
+        return [
486
+            ...userDisplayItems,
487
+            ...numberDisplayItems
488
+        ];
489
+    }
490
+
491
+    _query: (string) => Promise<Array<Object>>;
492
+
493
+    /**
494
+     * Performs a people and phone number search request.
495
+     *
496
+     * @param {string} query - The search text.
497
+     * @private
498
+     * @returns {Promise}
499
+     */
500
+    _query(query = '') {
501
+        const text = query.trim();
502
+        const {
503
+            _dialOutAuthUrl,
504
+            _jwt,
505
+            _peopleSearchQueryTypes,
506
+            _peopleSearchUrl
507
+        } = this.props;
508
+
509
+        let peopleSearchPromise;
510
+
511
+        if (this.props.enableAddPeople) {
512
+            peopleSearchPromise = searchDirectory(
513
+                _peopleSearchUrl,
514
+                _jwt,
515
+                text,
516
+                _peopleSearchQueryTypes);
517
+        } else {
518
+            peopleSearchPromise = Promise.resolve([]);
519
+        }
520
+
521
+
522
+        const hasCountryCode = text.startsWith('+');
523
+        let phoneNumberPromise;
524
+
525
+        if (this.props.enableDialOut && this._isMaybeAPhoneNumber(text)) {
526
+            let numberToVerify = text;
527
+
528
+            // When the number to verify does not start with a +, we assume no
529
+            // proper country code has been entered. In such a case, prepend 1
530
+            // for the country code. The service currently takes care of
531
+            // prepending the +.
532
+            if (!hasCountryCode && !text.startsWith('1')) {
533
+                numberToVerify = `1${numberToVerify}`;
534
+            }
535
+
536
+            // The validation service works properly when the query is digits
537
+            // only so ensure only digits get sent.
538
+            numberToVerify = this._getDigitsOnly(numberToVerify);
539
+
540
+            phoneNumberPromise
541
+                = checkDialNumber(numberToVerify, _dialOutAuthUrl);
542
+        } else {
543
+            phoneNumberPromise = Promise.resolve({});
250
         }
544
         }
545
+
546
+        return Promise.all([ peopleSearchPromise, phoneNumberPromise ])
547
+            .then(([ peopleResults, phoneResults ]) => {
548
+                const results = [
549
+                    ...peopleResults
550
+                ];
551
+
552
+                /**
553
+                 * This check for phone results is for the day the call to
554
+                 * searching people might return phone results as well. When
555
+                 * that day comes this check will make it so the server checks
556
+                 * are honored and the local appending of the number is not
557
+                 * done. The local appending of the phone number can then be
558
+                 * cleaned up when convenient.
559
+                 */
560
+                const hasPhoneResult = peopleResults.find(
561
+                    result => result.type === 'phone');
562
+
563
+                if (!hasPhoneResult
564
+                        && typeof phoneResults.allow === 'boolean') {
565
+                    results.push({
566
+                        allowed: phoneResults.allow,
567
+                        country: phoneResults.country,
568
+                        type: 'phone',
569
+                        number: phoneResults.phone,
570
+                        originalEntry: text,
571
+                        showCountryCodeReminder: !hasCountryCode
572
+                    });
573
+                }
574
+
575
+                return results;
576
+            });
251
     }
577
     }
252
 
578
 
253
     /**
579
     /**
294
     }
620
     }
295
 
621
 
296
     /**
622
     /**
297
-     * Renders the input form.
623
+     * Renders a telephone icon.
298
      *
624
      *
299
      * @private
625
      * @private
300
      * @returns {ReactElement}
626
      * @returns {ReactElement}
301
      */
627
      */
302
-    _renderUserInputForm() {
303
-        const { t } = this.props;
304
-
628
+    _renderTelephoneIcon() {
305
         return (
629
         return (
306
-            <div className = 'add-people-form-wrap'>
307
-                { this._renderErrorMessage() }
308
-                <MultiSelectAutocomplete
309
-                    isDisabled
310
-                        = { this.state.addToCallInProgress || false }
311
-                    noMatchesFound = { t('addPeople.noResults') }
312
-                    onSelectionChange = { this._onSelectionChange }
313
-                    placeholder = { t('addPeople.searchPlaceholder') }
314
-                    ref = { this._setMultiSelectElement }
315
-                    resourceClient = { this._resourceClient }
316
-                    shouldFitContainer = { true }
317
-                    shouldFocus = { true } />
318
-            </div>
630
+            <span className = 'add-telephone-icon'>
631
+                <i className = 'icon-telephone' />
632
+            </span>
319
         );
633
         );
320
     }
634
     }
321
 
635
 
341
  * @param {Object} state - The Redux state.
655
  * @param {Object} state - The Redux state.
342
  * @private
656
  * @private
343
  * @returns {{
657
  * @returns {{
658
+ *     _conference: Object,
659
+ *     _dialOutAuthUrl: string,
660
+ *     _inviteServiceUrl: string,
661
+ *     _inviteUrl: string,
344
  *     _jwt: string,
662
  *     _jwt: string,
663
+ *     _peopleSearchQueryTypes: Array<string>,
345
  *     _peopleSearchUrl: string
664
  *     _peopleSearchUrl: string
346
  * }}
665
  * }}
347
  */
666
  */
348
 function _mapStateToProps(state) {
667
 function _mapStateToProps(state) {
349
     const { conference } = state['features/base/conference'];
668
     const { conference } = state['features/base/conference'];
350
     const {
669
     const {
670
+        dialOutAuthUrl,
351
         inviteServiceUrl,
671
         inviteServiceUrl,
352
         peopleSearchQueryTypes,
672
         peopleSearchQueryTypes,
353
         peopleSearchUrl
673
         peopleSearchUrl
355
 
675
 
356
     return {
676
     return {
357
         _conference: conference,
677
         _conference: conference,
678
+        _dialOutAuthUrl: dialOutAuthUrl,
358
         _inviteServiceUrl: inviteServiceUrl,
679
         _inviteServiceUrl: inviteServiceUrl,
359
         _inviteUrl: getInviteURL(state),
680
         _inviteUrl: getInviteURL(state),
360
         _jwt: state['features/base/jwt'].jwt,
681
         _jwt: state['features/base/jwt'].jwt,

+ 18
- 159
react/features/invite/components/InviteButton.web.js View File

1
-/* global interfaceConfig */
2
-
3
 import PropTypes from 'prop-types';
1
 import PropTypes from 'prop-types';
4
 import React, { Component } from 'react';
2
 import React, { Component } from 'react';
5
 import { connect } from 'react-redux';
3
 import { connect } from 'react-redux';
6
 import Button from '@atlaskit/button';
4
 import Button from '@atlaskit/button';
7
-import DropdownMenu from '@atlaskit/dropdown-menu';
8
-
9
-import { translate } from '../../base/i18n';
10
-import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
11
 
5
 
12
 import { openDialog } from '../../base/dialog';
6
 import { openDialog } from '../../base/dialog';
7
+import { translate } from '../../base/i18n';
13
 import { AddPeopleDialog } from '.';
8
 import { AddPeopleDialog } from '.';
14
-import { DialOutDialog } from '../../dial-out';
15
-import { isInviteOptionEnabled } from '../functions';
16
-
17
-const DIAL_OUT_OPTION = 'dialout';
18
-const ADD_TO_CALL_OPTION = 'addtocall';
19
 
9
 
20
 /**
10
 /**
21
  * The button that provides different invite options.
11
  * The button that provides different invite options.
28
      */
18
      */
29
     static propTypes = {
19
     static propTypes = {
30
         /**
20
         /**
31
-         * Indicates if the "Add to call" feature is available.
21
+         * Invoked to open {@code AddPeopleDialog}.
32
          */
22
          */
33
-        _isAddToCallAvailable: PropTypes.bool,
23
+        dispatch: PropTypes.func,
34
 
24
 
35
         /**
25
         /**
36
-         * Indicates if the "Dial out" feature is available.
26
+         * Indicates if the "Add to call" feature is available.
37
          */
27
          */
38
-        _isDialOutAvailable: PropTypes.bool,
28
+        enableAddPeople: PropTypes.bool,
39
 
29
 
40
         /**
30
         /**
41
-         * The function opening the dialog.
31
+         * Indicates if the "Dial out" feature is available.
42
          */
32
          */
43
-        openDialog: PropTypes.func,
33
+        enableDialOut: PropTypes.bool,
44
 
34
 
45
         /**
35
         /**
46
          * Invoked to obtain translated strings.
36
          * Invoked to obtain translated strings.
57
     constructor(props) {
47
     constructor(props) {
58
         super(props);
48
         super(props);
59
 
49
 
60
-        this._onInviteOptionSelected = this._onInviteOptionSelected.bind(this);
61
-        this._updateInviteItems = this._updateInviteItems.bind(this);
62
-
63
-        this._updateInviteItems(this.props);
64
-    }
65
-
66
-    /**
67
-     * Implements React's {@link Component#componentWillReceiveProps()}.
68
-     *
69
-     * @inheritdoc
70
-     * @param {Object} nextProps - The read-only props which this Component will
71
-     * receive.
72
-     * @returns {void}
73
-     */
74
-    componentWillReceiveProps(nextProps) {
75
-        if (this.props._isDialOutAvailable !== nextProps._isDialOutAvailable
76
-                || this.props._isAddToCallAvailable
77
-                    !== nextProps._isAddToCallAvailable) {
78
-            this._updateInviteItems(nextProps);
79
-        }
50
+        // Bind event handler so it is only bound once for every instance.
51
+        this._onClick = this._onClick.bind(this);
80
     }
52
     }
81
 
53
 
82
     /**
54
     /**
85
      * @returns {ReactElement}
57
      * @returns {ReactElement}
86
      */
58
      */
87
     render() {
59
     render() {
88
-        // HACK ALERT: Normally children should not be controlling their own
89
-        // visibility; parents should control that. However, this component is
90
-        // in a transitionary state while the Invite Dialog is being redone.
91
-        // This hack will go away when the Invite Dialog is back.
92
-        if (!this.state.buttonOption) {
93
-            return null;
94
-        }
95
-
96
-        const { VERTICAL_FILMSTRIP } = interfaceConfig;
97
-
98
         return (
60
         return (
99
             <div className = 'filmstrip__invite'>
61
             <div className = 'filmstrip__invite'>
100
                 <div className = 'invite-button-group'>
62
                 <div className = 'invite-button-group'>
101
                     <Button
63
                     <Button
102
-                        // eslint-disable-next-line react/jsx-handler-names
103
-                        onClick = { this.state.buttonOption.action }
64
+                        onClick = { this._onClick }
104
                         shouldFitContainer = { true }>
65
                         shouldFitContainer = { true }>
105
-                        { this.state.buttonOption.content }
66
+                        { this.props.t('addPeople.invite') }
106
                     </Button>
67
                     </Button>
107
-                    { this.state.inviteOptions[0].items.length
108
-                        ? <DropdownMenu
109
-                            items = { this.state.inviteOptions }
110
-                            onItemActivated = { this._onInviteOptionSelected }
111
-                            position = { VERTICAL_FILMSTRIP
112
-                                ? 'bottom right'
113
-                                : 'top right' }
114
-                            shouldFlip = { true }
115
-                            triggerType = 'button' />
116
-                        : null }
117
                 </div>
68
                 </div>
118
             </div>
69
             </div>
119
         );
70
         );
120
     }
71
     }
121
 
72
 
122
     /**
73
     /**
123
-     * Handles selection of the invite options.
124
-     *
125
-     * @param { Object } option - The invite option that has been selected from
126
-     * the dropdown menu.
127
-     * @private
128
-     * @returns {void}
129
-     */
130
-    _onInviteOptionSelected(option) {
131
-        this.state.inviteOptions[0].items.forEach(item => {
132
-            if (item.content === option.item.content) {
133
-                item.action();
134
-            }
135
-        });
136
-    }
137
-
138
-    /**
139
-     * Updates the invite items list depending on the availability of the
140
-     * features.
74
+     * Opens {@code AddPeopleDialog}.
141
      *
75
      *
142
-     * @param {Object} props - The read-only properties of the component.
143
      * @private
76
      * @private
144
      * @returns {void}
77
      * @returns {void}
145
      */
78
      */
146
-    _updateInviteItems(props) {
147
-        const { INVITE_OPTIONS = [] } = interfaceConfig;
148
-        const validOptions = INVITE_OPTIONS.filter(option =>
149
-            (option === DIAL_OUT_OPTION && props._isDialOutAvailable)
150
-            || (option === ADD_TO_CALL_OPTION && props._isAddToCallAvailable));
151
-
152
-        /* eslint-disable array-callback-return */
153
-
154
-        const inviteItems = validOptions.map(option => {
155
-            switch (option) {
156
-            case DIAL_OUT_OPTION:
157
-                return {
158
-                    content: this.props.t('dialOut.dialOut'),
159
-                    action: () => this.props.openDialog(DialOutDialog)
160
-                };
161
-            case ADD_TO_CALL_OPTION:
162
-                return {
163
-                    content: interfaceConfig.ADD_PEOPLE_APP_NAME,
164
-                    action: () => this.props.openDialog(AddPeopleDialog)
165
-                };
166
-            }
167
-        });
168
-
169
-        /* eslint-enable array-callback-return */
170
-
171
-        const buttonOption = inviteItems[0];
172
-        const dropdownOptions = inviteItems.splice(1, inviteItems.length);
173
-
174
-        const nextState = {
175
-            /**
176
-             * The configuration for how the invite button should display and
177
-             * behave on click.
178
-             */
179
-            buttonOption,
180
-
181
-            /**
182
-             * The list of invite options in the dropdown.
183
-             */
184
-            inviteOptions: [
185
-                {
186
-                    items: dropdownOptions
187
-                }
188
-            ]
189
-        };
190
-
191
-        if (this.state) {
192
-            this.setState(nextState);
193
-        } else {
194
-            // eslint-disable-next-line react/no-direct-mutation-state
195
-            this.state = nextState;
196
-        }
79
+    _onClick() {
80
+        this.props.dispatch(openDialog(AddPeopleDialog, {
81
+            enableAddPeople: this.props.enableAddPeople,
82
+            enableDialOut: this.props.enableDialOut
83
+        }));
197
     }
84
     }
198
 }
85
 }
199
 
86
 
200
-/**
201
- * Maps (parts of) the Redux state to the associated {@code InviteButton}'s
202
- * props.
203
- *
204
- * @param {Object} state - The Redux state.
205
- * @private
206
- * @returns {{
207
- *     _isAddToCallAvailable: boolean,
208
- *     _isDialOutAvailable: boolean
209
- * }}
210
- */
211
-function _mapStateToProps(state) {
212
-    const { conference } = state['features/base/conference'];
213
-    const { enableUserRolesBasedOnToken } = state['features/base/config'];
214
-    const { isGuest } = state['features/base/jwt'];
215
-
216
-    return {
217
-        _isAddToCallAvailable:
218
-            !isGuest && isInviteOptionEnabled(ADD_TO_CALL_OPTION),
219
-        _isDialOutAvailable:
220
-            getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR
221
-                && conference && conference.isSIPCallingSupported()
222
-                && isInviteOptionEnabled(DIAL_OUT_OPTION)
223
-                && (!enableUserRolesBasedOnToken || !isGuest)
224
-    };
225
-}
226
-
227
-export default translate(connect(_mapStateToProps, { openDialog })(
228
-    InviteButton));
87
+export default translate(connect()(InviteButton));

+ 29
- 1
react/features/invite/functions.js View File

75
         jwt: string,
75
         jwt: string,
76
         text: string,
76
         text: string,
77
         queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
77
         queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
78
-): Promise<void> {
78
+): Promise<Array<Object>> {
79
     const queryTypesString = JSON.stringify(queryTypes);
79
     const queryTypesString = JSON.stringify(queryTypes);
80
 
80
 
81
     return new Promise((resolve, reject) => {
81
     return new Promise((resolve, reject) => {
86
             .catch((jqxhr, textStatus, error) => reject(error));
86
             .catch((jqxhr, textStatus, error) => reject(error));
87
     });
87
     });
88
 }
88
 }
89
+
90
+/**
91
+ * Sends an ajax request to check if the phone number can be called.
92
+ *
93
+ * @param {string} dialNumber - The dial number to check for validity.
94
+ * @param {string} dialOutAuthUrl - The endpoint to use for checking validity.
95
+ * @returns {Promise} - The promise created by the request.
96
+ */
97
+export function checkDialNumber(
98
+        dialNumber: string, dialOutAuthUrl: string): Promise<Object> {
99
+    if (!dialOutAuthUrl) {
100
+        // no auth url, let's say it is valid
101
+        const response = {
102
+            allow: true,
103
+            phone: dialNumber
104
+        };
105
+
106
+        return Promise.resolve(response);
107
+    }
108
+
109
+    const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`;
110
+
111
+    return new Promise((resolve, reject) => {
112
+        $.getJSON(fullUrl)
113
+            .then(resolve)
114
+            .catch(reject);
115
+    });
116
+}

Loading…
Cancel
Save