Преглед на файлове

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 години
родител
ревизия
4e4713c3e2

+ 0
- 81
css/_dial-out.scss Целия файл

@@ -1,81 +0,0 @@
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 Целия файл

@@ -1,35 +0,0 @@
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 Целия файл

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

+ 4
- 10
css/modals/invite/_add-people.scss Целия файл

@@ -11,17 +11,11 @@
11 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 Целия файл

@@ -1,9 +0,0 @@
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 Целия файл

@@ -1,6 +0,0 @@
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 Целия файл

@@ -1,5 +0,0 @@
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 Целия файл

@@ -1,7 +0,0 @@
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 Целия файл

@@ -1,15 +0,0 @@
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 Целия файл

@@ -1,18 +0,0 @@
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 Целия файл

@@ -452,17 +452,24 @@
452 452
         "qualityButtonTip": "Change received video quality"
453 453
     },
454 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 457
     "addPeople": {
462 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 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 473
         "failedToAdd": "Failed to add members"
467 474
     },
468 475
     "inlineDialogFailure": {

+ 21
- 38
react/features/base/react/components/web/MultiSelectAutocomplete.js Целия файл

@@ -1,6 +1,5 @@
1 1
 import { MultiSelectStateless } from '@atlaskit/multi-select';
2 2
 import AKInlineDialog from '@atlaskit/inline-dialog';
3
-import Spinner from '@atlaskit/spinner';
4 3
 import _debounce from 'lodash/debounce';
5 4
 import PropTypes from 'prop-types';
6 5
 import React, { Component } from 'react';
@@ -28,11 +27,22 @@ class MultiSelectAutocomplete extends Component {
28 27
          */
29 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 36
          * The text to show when no matches are found.
33 37
          */
34 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 47
          * The function called when the selection changes.
38 48
          */
@@ -113,14 +123,14 @@ class MultiSelectAutocomplete extends Component {
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 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,8 +150,10 @@ class MultiSelectAutocomplete extends Component {
140 150
                 <MultiSelectStateless
141 151
                     filterValue = { this.state.filterValue }
142 152
                     isDisabled = { isDisabled }
153
+                    isLoading = { this.state.loading }
143 154
                     isOpen = { this.state.isOpen }
144 155
                     items = { this.state.items }
156
+                    loadingMessage = { this.props.loadingMessage }
145 157
                     noMatchesFound = { noMatchesFound }
146 158
                     onFilterChange = { this._onFilterChange }
147 159
                     onRemoved = { this._onSelectionChange }
@@ -150,7 +162,6 @@ class MultiSelectAutocomplete extends Component {
150 162
                     selectedItems = { this.state.selectedItems }
151 163
                     shouldFitContainer = { shouldFitContainer }
152 164
                     shouldFocus = { shouldFocus } />
153
-                { this._renderLoadingIndicator() }
154 165
                 { this._renderError() }
155 166
             </div>
156 167
         );
@@ -169,7 +180,8 @@ class MultiSelectAutocomplete extends Component {
169 180
             error: this.state.error && Boolean(filterValue),
170 181
             filterValue,
171 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 186
         if (filterValue) {
175 187
             this._sendQuery(filterValue);
@@ -201,7 +213,7 @@ class MultiSelectAutocomplete extends Component {
201 213
         if (existing) {
202 214
             selectedItems = selectedItems.filter(k => k !== existing);
203 215
         } else {
204
-            selectedItems.push(item);
216
+            selectedItems.push(this.props.onItemSelected(item));
205 217
         }
206 218
         this.setState({
207 219
             isOpen: false,
@@ -236,33 +248,6 @@ class MultiSelectAutocomplete extends Component {
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 252
      * Sends a query to the resourceClient.
268 253
      *
@@ -275,7 +260,6 @@ class MultiSelectAutocomplete extends Component {
275 260
         }
276 261
 
277 262
         this.setState({
278
-            loading: true,
279 263
             error: false
280 264
         });
281 265
 
@@ -288,7 +272,6 @@ class MultiSelectAutocomplete extends Component {
288 272
             .then(results => {
289 273
                 if (this.state.filterValue !== filterValue) {
290 274
                     this.setState({
291
-                        loading: false,
292 275
                         error: false
293 276
                     });
294 277
 

+ 0
- 46
react/features/dial-out/actionTypes.js Целия файл

@@ -1,46 +0,0 @@
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 Целия файл

@@ -1,102 +0,0 @@
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 Целия файл

@@ -1,39 +0,0 @@
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 Целия файл


+ 0
- 248
react/features/dial-out/components/DialOutDialog.web.js Целия файл

@@ -1,248 +0,0 @@
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 Целия файл

@@ -1,369 +0,0 @@
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 Целия файл

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

+ 0
- 4
react/features/dial-out/index.js Целия файл

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

+ 0
- 53
react/features/dial-out/reducer.js Целия файл

@@ -1,53 +0,0 @@
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 Целия файл

@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
5 5
 import React, { Component } from 'react';
6 6
 import { connect } from 'react-redux';
7 7
 
8
+import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
8 9
 import { InviteButton } from '../../invite';
9 10
 import { Toolbox } from '../../toolbox';
10 11
 
@@ -43,6 +44,18 @@ class Filmstrip extends Component<*> {
43 44
          */
44 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 60
          * Whether or not the remote videos should be visible. Will toggle
48 61
          * a class for hiding the videos.
@@ -93,6 +106,14 @@ class Filmstrip extends Component<*> {
93 106
      * @returns {ReactElement}
94 107
      */
95 108
     render() {
109
+        const {
110
+            _hideInviteButton,
111
+            _isAddToCallAvailable,
112
+            _isDialOutAvailable,
113
+            _remoteVideosVisible,
114
+            filmstripOnly
115
+        } = this.props;
116
+
96 117
         /**
97 118
          * Note: Appending of {@code RemoteVideo} views is handled through
98 119
          * VideoLayout. The views do not get blown away on render() because
@@ -102,12 +123,12 @@ class Filmstrip extends Component<*> {
102 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 129
         return (
109 130
             <div className = { filmstripClassNames }>
110
-                { this.props.filmstripOnly ? <Toolbox /> : null }
131
+                { filmstripOnly ? <Toolbox /> : null }
111 132
                 <div
112 133
                     className = 'filmstrip__videos'
113 134
                     id = 'remoteVideos'>
@@ -116,9 +137,11 @@ class Filmstrip extends Component<*> {
116 137
                         id = 'filmstripLocalVideo'
117 138
                         onMouseOut = { this._onMouseOut }
118 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 145
                         <div id = 'filmstripLocalVideoThumbnail' />
123 146
                     </div>
124 147
                     <div
@@ -192,15 +215,34 @@ class Filmstrip extends Component<*> {
192 215
  * @param {Object} state - The Redux state.
193 216
  * @private
194 217
  * @returns {{
195
- *     _hovered: boolean,
196 218
  *     _hideInviteButton: boolean,
219
+ *     _hovered: boolean,
220
+ *     _isAddToCallAvailable: boolean,
221
+ *     _isDialOutAvailable: boolean,
197 222
  *     _remoteVideosVisible: boolean
198 223
  * }}
199 224
  */
200 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 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 246
         _remoteVideosVisible: shouldRemoteVideosBeVisible(state)
205 247
     };
206 248
 }

+ 395
- 74
react/features/invite/components/AddPeopleDialog.web.js Целия файл

@@ -11,12 +11,21 @@ import { getInviteURL } from '../../base/connection';
11 11
 import { Dialog, hideDialog } from '../../base/dialog';
12 12
 import { translate } from '../../base/i18n';
13 13
 import { MultiSelectAutocomplete } from '../../base/react';
14
-
15
-import { invitePeopleAndChatRooms, searchDirectory } from '../functions';
16 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 24
 declare var interfaceConfig: Object;
19 25
 
26
+const isPhoneNumberRegex
27
+    = new RegExp(interfaceConfig.PHONE_NUMBER_REGEX || '^[0-9+()-\\s]*$');
28
+
20 29
 /**
21 30
  * The dialog that allows to invite people to the call.
22 31
  */
@@ -33,6 +42,11 @@ class AddPeopleDialog extends Component<*, *> {
33 42
          */
34 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 51
          * The URL pointing to the service allowing for people invite.
38 52
          */
@@ -58,6 +72,16 @@ class AddPeopleDialog extends Component<*, *> {
58 72
          */
59 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 86
          * The function closing the dialog.
63 87
          */
@@ -76,33 +100,7 @@ class AddPeopleDialog extends Component<*, *> {
76 100
 
77 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 105
     state = {
108 106
         /**
@@ -116,6 +114,12 @@ class AddPeopleDialog extends Component<*, *> {
116 114
          */
117 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 124
          * The list of invite items.
121 125
          */
@@ -133,9 +137,17 @@ class AddPeopleDialog extends Component<*, *> {
133 137
 
134 138
         // Bind event handlers so they are only bound once per instance.
135 139
         this._isAddDisabled = this._isAddDisabled.bind(this);
140
+        this._onItemSelected = this._onItemSelected.bind(this);
136 141
         this._onSelectionChange = this._onSelectionChange.bind(this);
137 142
         this._onSubmit = this._onSubmit.bind(this);
143
+        this._parseQueryResults = this._parseQueryResults.bind(this);
144
+        this._query = this._query.bind(this);
138 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,7 +165,7 @@ class AddPeopleDialog extends Component<*, *> {
153 165
                 && !this.state.addToCallInProgress
154 166
                 && !this.state.addToCallError
155 167
                 && this._multiselect) {
156
-            this._multiselect.clear();
168
+            this._multiselect.setSelectedItems([]);
157 169
         }
158 170
     }
159 171
 
@@ -163,18 +175,69 @@ class AddPeopleDialog extends Component<*, *> {
163 175
      * @returns {ReactElement}
164 176
      */
165 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 202
         return (
167 203
             <Dialog
168 204
                 okDisabled = { this._isAddDisabled() }
169 205
                 okTitleKey = 'addPeople.add'
170 206
                 onSubmit = { this._onSubmit }
171 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 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 241
     _isAddDisabled: () => boolean;
179 242
 
180 243
     /**
@@ -189,6 +252,45 @@ class AddPeopleDialog extends Component<*, *> {
189 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 294
     _onSelectionChange: (Map<*, *>) => void;
193 295
 
194 296
     /**
@@ -199,55 +301,279 @@ class AddPeopleDialog extends Component<*, *> {
199 301
      * @returns {void}
200 302
      */
201 303
     _onSelectionChange(selectedItems) {
202
-        const selectedIds = selectedItems.map(o => o.item);
203
-
204 304
         this.setState({
205
-            inviteItems: selectedIds
305
+            inviteItems: selectedItems
206 306
         });
207 307
     }
208 308
 
209 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 319
      * @private
215 320
      * @returns {void}
216 321
      */
217 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 389
             this.props._conference
227 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 405
                     this.setState({
246 406
                         addToCallInProgress: false,
247 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,28 +620,16 @@ class AddPeopleDialog extends Component<*, *> {
294 620
     }
295 621
 
296 622
     /**
297
-     * Renders the input form.
623
+     * Renders a telephone icon.
298 624
      *
299 625
      * @private
300 626
      * @returns {ReactElement}
301 627
      */
302
-    _renderUserInputForm() {
303
-        const { t } = this.props;
304
-
628
+    _renderTelephoneIcon() {
305 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,13 +655,19 @@ class AddPeopleDialog extends Component<*, *> {
341 655
  * @param {Object} state - The Redux state.
342 656
  * @private
343 657
  * @returns {{
658
+ *     _conference: Object,
659
+ *     _dialOutAuthUrl: string,
660
+ *     _inviteServiceUrl: string,
661
+ *     _inviteUrl: string,
344 662
  *     _jwt: string,
663
+ *     _peopleSearchQueryTypes: Array<string>,
345 664
  *     _peopleSearchUrl: string
346 665
  * }}
347 666
  */
348 667
 function _mapStateToProps(state) {
349 668
     const { conference } = state['features/base/conference'];
350 669
     const {
670
+        dialOutAuthUrl,
351 671
         inviteServiceUrl,
352 672
         peopleSearchQueryTypes,
353 673
         peopleSearchUrl
@@ -355,6 +675,7 @@ function _mapStateToProps(state) {
355 675
 
356 676
     return {
357 677
         _conference: conference,
678
+        _dialOutAuthUrl: dialOutAuthUrl,
358 679
         _inviteServiceUrl: inviteServiceUrl,
359 680
         _inviteUrl: getInviteURL(state),
360 681
         _jwt: state['features/base/jwt'].jwt,

+ 18
- 159
react/features/invite/components/InviteButton.web.js Целия файл

@@ -1,21 +1,11 @@
1
-/* global interfaceConfig */
2
-
3 1
 import PropTypes from 'prop-types';
4 2
 import React, { Component } from 'react';
5 3
 import { connect } from 'react-redux';
6 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 6
 import { openDialog } from '../../base/dialog';
7
+import { translate } from '../../base/i18n';
13 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 11
  * The button that provides different invite options.
@@ -28,19 +18,19 @@ class InviteButton extends Component {
28 18
      */
29 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 36
          * Invoked to obtain translated strings.
@@ -57,26 +47,8 @@ class InviteButton extends Component {
57 47
     constructor(props) {
58 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,144 +57,31 @@ class InviteButton extends Component {
85 57
      * @returns {ReactElement}
86 58
      */
87 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 60
         return (
99 61
             <div className = 'filmstrip__invite'>
100 62
                 <div className = 'invite-button-group'>
101 63
                     <Button
102
-                        // eslint-disable-next-line react/jsx-handler-names
103
-                        onClick = { this.state.buttonOption.action }
64
+                        onClick = { this._onClick }
104 65
                         shouldFitContainer = { true }>
105
-                        { this.state.buttonOption.content }
66
+                        { this.props.t('addPeople.invite') }
106 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 68
                 </div>
118 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 76
      * @private
144 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 Целия файл

@@ -75,7 +75,7 @@ export function searchDirectory( // eslint-disable-line max-params
75 75
         jwt: string,
76 76
         text: string,
77 77
         queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
78
-): Promise<void> {
78
+): Promise<Array<Object>> {
79 79
     const queryTypesString = JSON.stringify(queryTypes);
80 80
 
81 81
     return new Promise((resolve, reject) => {
@@ -86,3 +86,31 @@ export function searchDirectory( // eslint-disable-line max-params
86 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…
Отказ
Запис