diff --git a/css/_base.scss b/css/_base.scss
index 68d855f4b..28d28759f 100644
--- a/css/_base.scss
+++ b/css/_base.scss
@@ -47,10 +47,6 @@ button, input, select, textarea {
button, select, input[type="button"],
input[type="reset"], input[type="submit"] {
- height: 32px;
- line-height: 32px;
- padding-left: 4px;
- padding-right: 4px;
cursor: pointer;
}
diff --git a/css/_font.scss b/css/_font.scss
index e1e466445..57e1c8a85 100644
--- a/css/_font.scss
+++ b/css/_font.scss
@@ -151,3 +151,6 @@
.icon-telephone:before {
content: "\e0cd";
}
+.icon-add:before {
+ content: "\e145";
+}
diff --git a/css/main.scss b/css/main.scss
index 43630b571..676f4c5c9 100644
--- a/css/main.scss
+++ b/css/main.scss
@@ -73,6 +73,7 @@
@import 'policy';
@import 'filmstrip';
@import 'unsupported-browser/main';
+@import 'modals/invite/add-people';
@import 'vertical_filmstrip_overrides';
/* Modules END */
diff --git a/css/modals/_dialog.scss b/css/modals/_dialog.scss
index c8ade0853..e4f6d5321 100644
--- a/css/modals/_dialog.scss
+++ b/css/modals/_dialog.scss
@@ -79,6 +79,7 @@
.modal-dialog-form {
color: $modalTextColor;
+ margin-top: 5px !important;
.input-control {
background: $modalMockAKInputBackground;
@@ -89,3 +90,21 @@
.modal-dialog-footer {
font-size: $modalButtonFontSize;
}
+
+/**
+ * Styling inline dialog errors.
+ */
+.inline-dialog-error {
+ margin-top: 16px;
+
+ &-text {
+ color: $dialogErrorText;
+ margin-bottom: 8px;
+ text-align: center;
+ }
+
+ &-button {
+ display: block;
+ margin: 16px auto 0 auto;
+ }
+}
diff --git a/css/modals/invite/_add-people.scss b/css/modals/invite/_add-people.scss
new file mode 100644
index 000000000..f1c62c408
--- /dev/null
+++ b/css/modals/invite/_add-people.scss
@@ -0,0 +1,32 @@
+/**
+ * Styles errors and links in the AddPeopleDialog.
+ */
+.modal-dialog-form {
+ .add-people-form-wrap {
+
+ .error {
+ padding-left: 5px;
+
+ a {
+ padding-left: 5px;
+ }
+ }
+ }
+}
+
+/**
+ * Styles the loading element in the MultiSelectAutocomplete.
+ */
+.autocomplete-loading {
+ justify-content: center;
+ display: flex;
+ min-width: 260px;
+ padding: 20px;
+}
+
+/**
+ * Styles errors in the MultiSelectAutocomplete.
+ */
+.autocomplete-error {
+ min-width: 260px;
+}
diff --git a/css/themes/_light.scss b/css/themes/_light.scss
index c0a93868e..be3f2533b 100644
--- a/css/themes/_light.scss
+++ b/css/themes/_light.scss
@@ -58,6 +58,7 @@ $auiDialogBg: #f5f5f5;
$auiDialogContentBg: $baseLight;
$auiBorderColor: #ccc;
$dialogTitleFontWeight: 400;
+$dialogErrorText: #344563;
/**
* Inlay colors
diff --git a/fonts/jitsi.eot b/fonts/jitsi.eot
index 5ef7d46b0..f369ce469 100644
Binary files a/fonts/jitsi.eot and b/fonts/jitsi.eot differ
diff --git a/fonts/jitsi.svg b/fonts/jitsi.svg
index 74ed425ad..23a820ec9 100644
--- a/fonts/jitsi.svg
+++ b/fonts/jitsi.svg
@@ -8,6 +8,7 @@
+
diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf
index 095d77135..65eaa9dcd 100644
Binary files a/fonts/jitsi.ttf and b/fonts/jitsi.ttf differ
diff --git a/fonts/jitsi.woff b/fonts/jitsi.woff
index 066f64da4..0a7405f09 100644
Binary files a/fonts/jitsi.woff and b/fonts/jitsi.woff differ
diff --git a/fonts/selection.json b/fonts/selection.json
index f3b9e25e3..5991a9ec4 100644
--- a/fonts/selection.json
+++ b/fonts/selection.json
@@ -1,62 +1,6 @@
{
"IcoMoonType": "selection",
"icons": [
- {
- "icon": {
- "paths": [
- "M330.667 554.667c-0.427-14.933 6.4-29.44 17.92-39.253 32 6.827 61.867 20.053 88.747 39.253 0 29.013-23.893 52.907-53.333 52.907s-52.907-23.467-53.333-52.907zM586.667 554.667c26.88-18.773 56.747-32 88.747-38.827 11.52 9.813 18.347 24.32 17.92 38.827 0 29.867-23.893 53.76-53.333 53.76s-53.333-23.893-53.333-53.76v0zM512 384c-118.187-1.707-234.667 27.733-338.347 85.333l-2.987 42.667c0 52.48 12.373 104.107 35.84 151.040 101.12-15.36 203.093-23.040 305.493-23.040s204.373 7.68 305.493 23.040c23.467-46.933 35.84-98.56 35.84-151.040l-2.987-42.667c-103.68-57.6-220.16-87.040-338.347-85.333zM512 85.333c235.641 0 426.667 191.025 426.667 426.667s-191.025 426.667-426.667 426.667c-235.641 0-426.667-191.025-426.667-426.667s191.025-426.667 426.667-426.667z"
- ],
- "attrs": [
- {}
- ],
- "isMulticolor": false,
- "isMulticolor2": false,
- "grid": 24,
- "tags": [
- "ninja"
- ]
- },
- "attrs": [
- {}
- ],
- "properties": {
- "order": 851,
- "id": 121,
- "name": "ninja",
- "prevSize": 32,
- "code": 59657
- },
- "setIdx": 0,
- "setId": 1,
- "iconIdx": 0
- },
- {
- "icon": {
- "paths": [
- "M282 460c62 120 162 220 282 282l94-94c12-12 30-16 44-10 48 16 100 24 152 24 24 0 42 18 42 42v150c0 24-18 42-42 42-400 0-726-326-726-726 0-24 18-42 42-42h150c24 0 42 18 42 42 0 54 8 104 24 152 4 14 2 32-10 44z"
- ],
- "attrs": [],
- "isMulticolor": false,
- "isMulticolor2": false,
- "tags": [
- "phone"
- ],
- "defaultCode": 57549,
- "grid": 24
- },
- "attrs": [],
- "properties": {
- "ligatures": "call, local_phone, phone",
- "id": 120,
- "order": 848,
- "prevSize": 32,
- "code": 57549,
- "name": "phone"
- },
- "setIdx": 0,
- "setId": 1,
- "iconIdx": 41
- },
{
"icon": {
"paths": [
@@ -76,15 +20,15 @@
{}
],
"properties": {
- "order": 109,
+ "order": 856,
"id": 0,
"name": "mic-camera-combined",
"prevSize": 32,
"code": 59651
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 1
+ "setId": 5,
+ "iconIdx": 0
},
{
"icon": {
@@ -105,15 +49,15 @@
{}
],
"properties": {
- "order": 104,
+ "order": 857,
"id": 1,
"name": "feedback",
"prevSize": 32,
"code": 59677
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 2
+ "setId": 5,
+ "iconIdx": 1
},
{
"icon": {
@@ -134,15 +78,15 @@
{}
],
"properties": {
- "order": 103,
+ "order": 858,
"id": 2,
"name": "toggle-filmstrip",
"prevSize": 32,
"code": 59676
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 3
+ "setId": 5,
+ "iconIdx": 2
},
{
"icon": {
@@ -160,15 +104,15 @@
"attrs": [],
"properties": {
"id": 3,
- "order": 60,
+ "order": 859,
"ligatures": "account_circle",
"prevSize": 32,
"code": 59649,
"name": "avatar"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 4
+ "setId": 5,
+ "iconIdx": 3
},
{
"icon": {
@@ -186,15 +130,15 @@
"attrs": [],
"properties": {
"id": 4,
- "order": 849,
+ "order": 860,
"ligatures": "call_end",
"prevSize": 32,
"code": 59653,
"name": "hangup"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 5
+ "setId": 5,
+ "iconIdx": 4
},
{
"icon": {
@@ -212,15 +156,15 @@
"attrs": [],
"properties": {
"id": 5,
- "order": 61,
+ "order": 861,
"ligatures": "chat_bubble_outline",
"prevSize": 32,
"code": 59654,
"name": "chat"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 6
+ "setId": 5,
+ "iconIdx": 5
},
{
"icon": {
@@ -238,15 +182,15 @@
"attrs": [],
"properties": {
"id": 6,
- "order": 99,
+ "order": 862,
"ligatures": "cloud_download",
"prevSize": 32,
"code": 59650,
"name": "download"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 7
+ "setId": 5,
+ "iconIdx": 6
},
{
"icon": {
@@ -264,15 +208,15 @@
"attrs": [],
"properties": {
"id": 7,
- "order": 89,
+ "order": 863,
"ligatures": "create, edit, mode_edit",
"prevSize": 32,
"code": 59655,
"name": "edit"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 8
+ "setId": 5,
+ "iconIdx": 7
},
{
"icon": {
@@ -290,15 +234,15 @@
"attrs": [],
"properties": {
"id": 8,
- "order": 85,
+ "order": 864,
"ligatures": "description",
"prevSize": 32,
"code": 59656,
"name": "share-doc"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 9
+ "setId": 5,
+ "iconIdx": 8
},
{
"icon": {
@@ -315,16 +259,16 @@
},
"attrs": [],
"properties": {
- "id": 10,
- "order": 98,
+ "id": 9,
+ "order": 865,
"ligatures": "eject",
"prevSize": 32,
"code": 59652,
"name": "kick"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 10
+ "setId": 5,
+ "iconIdx": 9
},
{
"icon": {
@@ -341,16 +285,16 @@
},
"attrs": [],
"properties": {
- "id": 11,
- "order": 106,
+ "id": 10,
+ "order": 866,
"ligatures": "expand_less",
"prevSize": 32,
"code": 59679,
"name": "menu-up"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 11
+ "setId": 5,
+ "iconIdx": 10
},
{
"icon": {
@@ -367,16 +311,16 @@
},
"attrs": [],
"properties": {
- "id": 12,
- "order": 107,
+ "id": 11,
+ "order": 867,
"ligatures": "expand_more",
"prevSize": 32,
"code": 59680,
"name": "menu-down"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 12
+ "setId": 5,
+ "iconIdx": 11
},
{
"icon": {
@@ -393,16 +337,16 @@
},
"attrs": [],
"properties": {
- "id": 13,
- "order": 94,
+ "id": 12,
+ "order": 868,
"ligatures": "fullscreen",
"prevSize": 32,
"code": 59659,
"name": "full-screen"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 13
+ "setId": 5,
+ "iconIdx": 12
},
{
"icon": {
@@ -419,16 +363,16 @@
},
"attrs": [],
"properties": {
- "id": 14,
- "order": 92,
+ "id": 13,
+ "order": 869,
"ligatures": "fullscreen_exit",
"prevSize": 32,
"code": 59660,
"name": "exit-full-screen"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 14
+ "setId": 5,
+ "iconIdx": 13
},
{
"icon": {
@@ -445,16 +389,16 @@
},
"attrs": [],
"properties": {
- "id": 15,
- "order": 101,
+ "id": 14,
+ "order": 870,
"ligatures": "grade, star",
"prevSize": 32,
"code": 59658,
"name": "star-full"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 15
+ "setId": 5,
+ "iconIdx": 14
},
{
"icon": {
@@ -471,16 +415,16 @@
},
"attrs": [],
"properties": {
- "id": 16,
- "order": 66,
+ "id": 15,
+ "order": 871,
"ligatures": "lock_open",
"prevSize": 32,
"code": 59661,
"name": "security"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 16
+ "setId": 5,
+ "iconIdx": 15
},
{
"icon": {
@@ -497,16 +441,16 @@
},
"attrs": [],
"properties": {
- "id": 17,
- "order": 65,
+ "id": 16,
+ "order": 872,
"ligatures": "lock_outline",
"prevSize": 32,
"code": 59662,
"name": "security-locked"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 17
+ "setId": 5,
+ "iconIdx": 16
},
{
"icon": {
@@ -523,16 +467,16 @@
},
"attrs": [],
"properties": {
- "id": 18,
- "order": 67,
+ "id": 17,
+ "order": 873,
"ligatures": "loop, sync",
"prevSize": 32,
"code": 59663,
"name": "reload"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 18
+ "setId": 5,
+ "iconIdx": 17
},
{
"icon": {
@@ -549,16 +493,16 @@
},
"attrs": [],
"properties": {
- "id": 19,
- "order": 68,
+ "id": 18,
+ "order": 874,
"ligatures": "mic",
"prevSize": 32,
"code": 59664,
"name": "microphone"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 19
+ "setId": 5,
+ "iconIdx": 18
},
{
"icon": {
@@ -575,16 +519,16 @@
},
"attrs": [],
"properties": {
- "id": 20,
- "order": 69,
+ "id": 19,
+ "order": 875,
"ligatures": "mic_none",
"prevSize": 32,
"code": 59665,
"name": "mic-empty"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 20
+ "setId": 5,
+ "iconIdx": 19
},
{
"icon": {
@@ -601,16 +545,16 @@
},
"attrs": [],
"properties": {
- "id": 21,
- "order": 70,
+ "id": 20,
+ "order": 876,
"ligatures": "mic_off",
"prevSize": 32,
"code": 59666,
"name": "mic-disabled"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 21
+ "setId": 5,
+ "iconIdx": 20
},
{
"icon": {
@@ -627,16 +571,16 @@
},
"attrs": [],
"properties": {
- "id": 22,
- "order": 105,
+ "id": 21,
+ "order": 877,
"ligatures": "pan_tool",
"prevSize": 32,
"code": 59678,
"name": "raised-hand"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 22
+ "setId": 5,
+ "iconIdx": 21
},
{
"icon": {
@@ -653,16 +597,16 @@
},
"attrs": [],
"properties": {
- "id": 23,
- "order": 100,
+ "id": 22,
+ "order": 878,
"ligatures": "people_outline",
"prevSize": 32,
"code": 59675,
"name": "contactList"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 23
+ "setId": 5,
+ "iconIdx": 22
},
{
"icon": {
@@ -679,16 +623,16 @@
},
"attrs": [],
"properties": {
- "id": 24,
- "order": 87,
+ "id": 23,
+ "order": 879,
"ligatures": "person_add",
"prevSize": 32,
"code": 59667,
"name": "link"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 24
+ "setId": 5,
+ "iconIdx": 23
},
{
"icon": {
@@ -705,16 +649,16 @@
},
"attrs": [],
"properties": {
- "id": 25,
- "order": 82,
+ "id": 24,
+ "order": 880,
"ligatures": "play_circle_outline",
"prevSize": 32,
"code": 59668,
"name": "shared-video"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 25
+ "setId": 5,
+ "iconIdx": 24
},
{
"icon": {
@@ -731,16 +675,16 @@
},
"attrs": [],
"properties": {
- "id": 26,
- "order": 81,
+ "id": 25,
+ "order": 881,
"ligatures": "settings",
"prevSize": 32,
"code": 59669,
"name": "settings"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 26
+ "setId": 5,
+ "iconIdx": 25
},
{
"icon": {
@@ -757,16 +701,16 @@
},
"attrs": [],
"properties": {
- "id": 27,
- "order": 76,
+ "id": 26,
+ "order": 882,
"ligatures": "star_border",
"prevSize": 32,
"code": 59670,
"name": "star"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 27
+ "setId": 5,
+ "iconIdx": 26
},
{
"icon": {
@@ -783,16 +727,16 @@
},
"attrs": [],
"properties": {
- "id": 28,
- "order": 108,
+ "id": 27,
+ "order": 883,
"ligatures": "switch_camera",
"prevSize": 32,
"code": 59681,
"name": "switch-camera"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 28
+ "setId": 5,
+ "iconIdx": 27
},
{
"icon": {
@@ -809,16 +753,16 @@
},
"attrs": [],
"properties": {
- "id": 29,
- "order": 93,
+ "id": 28,
+ "order": 884,
"ligatures": "tv",
"prevSize": 32,
"code": 59671,
"name": "share-desktop"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 29
+ "setId": 5,
+ "iconIdx": 28
},
{
"icon": {
@@ -835,16 +779,16 @@
},
"attrs": [],
"properties": {
- "id": 30,
- "order": 77,
+ "id": 29,
+ "order": 885,
"ligatures": "videocam",
"prevSize": 32,
"code": 59672,
"name": "camera"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 30
+ "setId": 5,
+ "iconIdx": 29
},
{
"icon": {
@@ -861,16 +805,16 @@
},
"attrs": [],
"properties": {
- "id": 31,
- "order": 78,
+ "id": 30,
+ "order": 886,
"ligatures": "videocam_off",
"prevSize": 32,
"code": 59673,
"name": "camera-disabled"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 31
+ "setId": 5,
+ "iconIdx": 30
},
{
"icon": {
@@ -887,16 +831,16 @@
},
"attrs": [],
"properties": {
- "id": 32,
- "order": 79,
+ "id": 31,
+ "order": 887,
"ligatures": "volume_up",
"prevSize": 32,
"code": 59674,
"name": "volume"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 32
+ "setId": 5,
+ "iconIdx": 31
},
{
"icon": {
@@ -936,15 +880,15 @@
{}
],
"properties": {
- "order": 33,
- "id": 33,
+ "order": 888,
+ "id": 32,
"name": "connection-lost",
"prevSize": 32,
"code": 59648
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 33
+ "setId": 5,
+ "iconIdx": 32
},
{
"icon": {
@@ -1008,16 +952,16 @@
}
],
"properties": {
- "order": 37,
- "id": 34,
+ "order": 889,
+ "id": 33,
"prevSize": 32,
"code": 58906,
"name": "connection",
"ligatures": ""
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 34
+ "setId": 5,
+ "iconIdx": 33
},
{
"icon": {
@@ -1037,16 +981,16 @@
},
"attrs": [],
"properties": {
- "order": 43,
- "id": 35,
+ "order": 890,
+ "id": 34,
"prevSize": 32,
"code": 58899,
"name": "recDisable",
"ligatures": ""
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 35
+ "setId": 5,
+ "iconIdx": 34
},
{
"icon": {
@@ -1067,16 +1011,16 @@
},
"attrs": [],
"properties": {
- "order": 44,
- "id": 36,
+ "order": 891,
+ "id": 35,
"prevSize": 32,
"code": 58900,
"name": "recEnable",
"ligatures": ""
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 36
+ "setId": 5,
+ "iconIdx": 35
},
{
"icon": {
@@ -1097,16 +1041,16 @@
},
"attrs": [],
"properties": {
- "order": 53,
- "id": 37,
+ "order": 892,
+ "id": 36,
"prevSize": 32,
"code": 58883,
"name": "presentation",
"ligatures": ""
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 37
+ "setId": 5,
+ "iconIdx": 36
},
{
"icon": {
@@ -1123,16 +1067,16 @@
},
"attrs": [],
"properties": {
- "order": 115,
+ "order": 893,
"ligatures": "dialpad",
- "id": 38,
+ "id": 37,
"prevSize": 32,
"code": 59685,
"name": "dialpad"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 38
+ "setId": 5,
+ "iconIdx": 37
},
{
"icon": {
@@ -1149,16 +1093,16 @@
},
"attrs": [],
"properties": {
- "order": 114,
+ "order": 894,
"ligatures": "remove_red_eye, visibility",
- "id": 39,
+ "id": 38,
"prevSize": 32,
"code": 59683,
"name": "visibility"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 39
+ "setId": 5,
+ "iconIdx": 38
},
{
"icon": {
@@ -1175,16 +1119,99 @@
},
"attrs": [],
"properties": {
- "order": 113,
+ "order": 895,
"ligatures": "visibility_off",
- "id": 40,
+ "id": 39,
"prevSize": 32,
"code": 59684,
"name": "visibility-off"
},
"setIdx": 0,
- "setId": 1,
- "iconIdx": 40
+ "setId": 5,
+ "iconIdx": 39
+ },
+ {
+ "icon": {
+ "paths": [
+ "M330.667 554.667c-0.427-14.933 6.4-29.44 17.92-39.253 32 6.827 61.867 20.053 88.747 39.253 0 29.013-23.893 52.907-53.333 52.907s-52.907-23.467-53.333-52.907zM586.667 554.667c26.88-18.773 56.747-32 88.747-38.827 11.52 9.813 18.347 24.32 17.92 38.827 0 29.867-23.893 53.76-53.333 53.76s-53.333-23.893-53.333-53.76v0zM512 384c-118.187-1.707-234.667 27.733-338.347 85.333l-2.987 42.667c0 52.48 12.373 104.107 35.84 151.040 101.12-15.36 203.093-23.040 305.493-23.040s204.373 7.68 305.493 23.040c23.467-46.933 35.84-98.56 35.84-151.040l-2.987-42.667c-103.68-57.6-220.16-87.040-338.347-85.333zM512 85.333c235.641 0 426.667 191.025 426.667 426.667s-191.025 426.667-426.667 426.667c-235.641 0-426.667-191.025-426.667-426.667s191.025-426.667 426.667-426.667z"
+ ],
+ "attrs": [
+ {}
+ ],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "tags": [
+ "ninja"
+ ],
+ "grid": 24
+ },
+ "attrs": [
+ {}
+ ],
+ "properties": {
+ "order": 850,
+ "id": 0,
+ "name": "ninja",
+ "prevSize": 32,
+ "code": 59657
+ },
+ "setIdx": 1,
+ "setId": 4,
+ "iconIdx": 0
+ },
+ {
+ "icon": {
+ "paths": [
+ "M282 460c62 120 162 220 282 282l94-94c12-12 30-16 44-10 48 16 100 24 152 24 24 0 42 18 42 42v150c0 24-18 42-42 42-400 0-726-326-726-726 0-24 18-42 42-42h150c24 0 42 18 42 42 0 54 8 104 24 152 4 14 2 32-10 44z"
+ ],
+ "attrs": [],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "tags": [
+ "phone"
+ ],
+ "defaultCode": 57549,
+ "grid": 24
+ },
+ "attrs": [],
+ "properties": {
+ "ligatures": "call, local_phone, phone",
+ "id": 1,
+ "order": 851,
+ "prevSize": 32,
+ "code": 57549,
+ "name": "phone"
+ },
+ "setIdx": 1,
+ "setId": 4,
+ "iconIdx": 1
+ },
+ {
+ "icon": {
+ "paths": [
+ "M810 554h-256v256h-84v-256h-256v-84h256v-256h84v256h256v84z"
+ ],
+ "attrs": [],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "tags": [
+ "add"
+ ],
+ "defaultCode": 57669,
+ "grid": 24
+ },
+ "attrs": [],
+ "properties": {
+ "ligatures": "add",
+ "id": 12,
+ "order": 896,
+ "prevSize": 32,
+ "code": 57669,
+ "name": "add"
+ },
+ "setIdx": 3,
+ "setId": 0,
+ "iconIdx": 12
}
],
"height": 1024,
diff --git a/interface_config.js b/interface_config.js
index fd4d0277e..d3be670a0 100644
--- a/interface_config.js
+++ b/interface_config.js
@@ -35,7 +35,7 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
//main toolbar
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'fodeviceselection', 'hangup', // jshint ignore:line
//extended toolbar
- 'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
+ 'profile', 'addtocall', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
/**
* Main Toolbar Buttons
* All of them should be in TOOLBAR_BUTTONS
@@ -91,6 +91,7 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
* @type {boolean}
*/
MOBILE_APP_PROMO: true,
+
/**
* Maximum coeficient of the ratio of the large video to the visible area
* after the large video is scaled to fit the window.
@@ -98,4 +99,10 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
* @type {number}
*/
MAXIMUM_ZOOMING_COEFFICIENT: 1.3
+
+ /*
+ * If indicated some of the error dialogs may point to the support URL for
+ * help.
+ */
+ // SUPPORT_URL: ""
};
diff --git a/lang/main.json b/lang/main.json
index aefe8a567..0ae1bb8e6 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -96,6 +96,7 @@
"rejoinKeyTitle": "Rejoin"
},
"toolbar": {
+ "addPeople": "Add people to your call",
"audioonly": "Enable / Disable audio only mode (saves bandwidth)",
"mute": "Mute / Unmute",
"videomute": "Start / Stop camera",
@@ -456,5 +457,17 @@
"statusMessage": "is now __status__",
"enterPhone": "Enter phone number",
"phoneNotAllowed": "Oh, we don't support that destination yet! Sorry!"
+ },
+ "addPeople": {
+ "add": "Add",
+ "noResults": "No matching search results",
+ "searchPlaceholder": "Search for people and rooms to add",
+ "title": "Add people to your call"
+ },
+ "inlineDialogFailure": {
+ "msg": "We stumbled a bit.",
+ "retry": "Try again",
+ "support": "Support",
+ "supportMsg": "If this keeps happening, reach out to"
}
}
diff --git a/package.json b/package.json
index 08a894967..b95938613 100644
--- a/package.json
+++ b/package.json
@@ -16,13 +16,17 @@
"readmeFilename": "README.md",
"//": "Callstats.io does not work with recent versions of jsSHA (2.0.1 in particular)",
"dependencies": {
- "@atlaskit/button": "1.0.3",
- "@atlaskit/button-group": "1.0.0",
+ "@atlaskit/avatar": "4.0.5",
+ "@atlaskit/button": "3.0.0",
+ "@atlaskit/button-group": "1.1.3",
"@atlaskit/dropdown-menu": "1.4.0",
"@atlaskit/field-text": "2.7.0",
- "@atlaskit/icon": "6.0.0",
- "@atlaskit/modal-dialog": "1.2.4",
- "@atlaskit/tabs": "1.2.5",
+ "@atlaskit/icon": "7.0.0",
+ "@atlaskit/inline-dialog": "3.2.0",
+ "@atlaskit/modal-dialog": "2.1.2",
+ "@atlaskit/multi-select": "6.2.0",
+ "@atlaskit/spinner": "2.2.3",
+ "@atlaskit/tabs": "2.0.0",
"@atlassian/aui": "6.0.6",
"async": "0.9.0",
"autosize": "1.18.13",
@@ -42,6 +46,7 @@
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "jitsi/lib-jitsi-meet",
"lodash": "4.17.4",
+ "nuclear-js": "1.4.0",
"postis": "2.2.0",
"react": "15.4.2",
"react-dom": "15.4.2",
diff --git a/react/features/base/dialog/components/StatelessDialog.web.js b/react/features/base/dialog/components/StatelessDialog.web.js
index ae9bd3fd7..aa19e4ee9 100644
--- a/react/features/base/dialog/components/StatelessDialog.web.js
+++ b/react/features/base/dialog/components/StatelessDialog.web.js
@@ -217,9 +217,9 @@ class StatelessDialog extends Component {
return (
-
+
{ this.props.titleString || t(this.props.titleKey) }
-
+
);
}
@@ -275,6 +275,9 @@ class StatelessDialog extends Component {
}
if (event.key === 'Enter') {
+ event.preventDefault();
+ event.stopPropagation();
+
if (this.props.submitDisabled && !this.props.cancelDisabled) {
this._onCancel();
} else if (!this.props.okDisabled) {
diff --git a/react/features/base/react/components/web/InlineDialogFailure.js b/react/features/base/react/components/web/InlineDialogFailure.js
new file mode 100644
index 000000000..654e1ef3b
--- /dev/null
+++ b/react/features/base/react/components/web/InlineDialogFailure.js
@@ -0,0 +1,72 @@
+import AKButton from '@atlaskit/button';
+import React, { Component } from 'react';
+
+import { translate } from '../../../i18n';
+
+declare var interfaceConfig: Object;
+
+/**
+ * Inline dialog that represents a failure and allows a retry.
+ */
+class InlineDialogFailure extends Component {
+ /**
+ * {@code InlineDialogFailure}'s property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Allows to retry the call that previously didn't succeed.
+ */
+ onRetry: React.PropTypes.func,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: React.PropTypes.func
+ };
+
+ /**
+ * Renders the content of this component.
+ *
+ * @returns {ReactElement}
+ */
+ render() {
+ const { t } = this.props;
+
+ const supportLink = interfaceConfig.SUPPORT_URL;
+ const supportLinkElem
+ = supportLink
+ ? ( // eslint-disable-line no-extra-parens
+
+ )
+ : null;
+
+ return (
+
+
+ { t('inlineDialogFailure.msg') }
+
+ { supportLinkElem }
+
+ { t('inlineDialogFailure.retry') }
+
+
+ );
+ }
+}
+
+export default translate(InlineDialogFailure);
diff --git a/react/features/base/react/components/web/MultiSelectAutocomplete.js b/react/features/base/react/components/web/MultiSelectAutocomplete.js
new file mode 100644
index 000000000..0ff8b1d40
--- /dev/null
+++ b/react/features/base/react/components/web/MultiSelectAutocomplete.js
@@ -0,0 +1,319 @@
+import { MultiSelectStateless } from '@atlaskit/multi-select';
+import AKInlineDialog from '@atlaskit/inline-dialog';
+import Spinner from '@atlaskit/spinner';
+import _debounce from 'lodash/debounce';
+import React, { Component } from 'react';
+
+import InlineDialogFailure from './InlineDialogFailure';
+
+/**
+ * A MultiSelect that is also auto-completing.
+ */
+class MultiSelectAutocomplete extends Component {
+
+ /**
+ * {@code MultiSelectAutocomplete} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * The default value of the selected item.
+ */
+ defaultValue: React.PropTypes.array,
+
+ /**
+ * Indicates if the component is disabled.
+ */
+ isDisabled: React.PropTypes.bool,
+
+ /**
+ * The text to show when no matches are found.
+ */
+ noMatchesFound: React.PropTypes.string,
+
+ /**
+ * The function called when the selection changes.
+ */
+ onSelectionChange: React.PropTypes.func,
+
+ /**
+ * The placeholder text of the input component.
+ */
+ placeholder: React.PropTypes.string,
+
+ /**
+ * The service providing the search.
+ */
+ resourceClient: React.PropTypes.shape({
+ makeQuery: React.PropTypes.func,
+ parseResults: React.PropTypes.func
+ }).isRequired,
+
+ /**
+ * Indicates if the component should fit the container.
+ */
+ shouldFitContainer: React.PropTypes.bool,
+
+ /**
+ * Indicates if we should focus.
+ */
+ shouldFocus: React.PropTypes.bool
+ };
+
+ /**
+ * Initializes a new {@code MultiSelectAutocomplete} instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ const defaultValue = this.props.defaultValue || [];
+
+ this.state = {
+ /**
+ * Indicates if the dropdown is open.
+ */
+ isOpen: false,
+
+ /**
+ * The text that filters the query result of the search.
+ */
+ filterValue: '',
+
+ /**
+ * Indicates if the component is currently loading results.
+ */
+ loading: false,
+
+
+ /**
+ * Indicates if there was an error.
+ */
+ error: false,
+
+ /**
+ * The list of result items.
+ */
+ items: [],
+
+ /**
+ * The list of selected items.
+ */
+ selectedItems: [ ...defaultValue ]
+ };
+
+ this._onFilterChange = this._onFilterChange.bind(this);
+ this._onRetry = this._onRetry.bind(this);
+ this._onSelectionChange = this._onSelectionChange.bind(this);
+ this._sendQuery = _debounce(this._sendQuery.bind(this), 200);
+ }
+
+ /**
+ * Clears the selected items.
+ *
+ * @returns {void}
+ */
+ clear() {
+ this.setState({
+ selectedItems: []
+ });
+ }
+
+ /**
+ * Renders the content of this component.
+ *
+ * @returns {ReactElement}
+ */
+ render() {
+ const shouldFitContainer = this.props.shouldFitContainer || false;
+ const shouldFocus = this.props.shouldFocus || false;
+ const isDisabled = this.props.isDisabled || false;
+ const placeholder = this.props.placeholder || '';
+ const noMatchesFound = this.props.noMatchesFound || '';
+
+ return (
+
+
+ { this._renderLoadingIndicator() }
+ { this._renderError() }
+
+ );
+ }
+
+ /**
+ * Sets the state and sends a query on filter change.
+ *
+ * @param {string} filterValue - The filter text value.
+ * @private
+ * @returns {void}
+ */
+ _onFilterChange(filterValue) {
+ this.setState({
+ // Clean the error if the filterValue is empty.
+ error: this.state.error && Boolean(filterValue),
+ filterValue,
+ isOpen: Boolean(this.state.items.length) && Boolean(filterValue),
+ items: filterValue ? this.state.items : []
+ });
+ if (filterValue) {
+ this._sendQuery(filterValue);
+ }
+ }
+
+ /**
+ * Retries the query on retry.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onRetry() {
+ this._sendQuery(this.state.filterValue);
+ }
+
+ /**
+ * Updates the selected items when a selection event occurs.
+ *
+ * @param {Object} item - The selected item.
+ * @private
+ * @returns {void}
+ */
+ _onSelectionChange(item) {
+ const existing
+ = this.state.selectedItems.find(k => k.value === item.value);
+ let selectedItems = this.state.selectedItems;
+
+ if (existing) {
+ selectedItems = selectedItems.filter(k => k !== existing);
+ } else {
+ selectedItems.push(item);
+ }
+ this.setState({
+ isOpen: false,
+ selectedItems
+ });
+
+ if (this.props.onSelectionChange) {
+ this.props.onSelectionChange(selectedItems);
+ }
+ }
+
+ /**
+ * Renders the error UI.
+ *
+ * @returns {ReactElement|null}
+ */
+ _renderError() {
+ if (!this.state.error) {
+ return null;
+ }
+ const content = ( // eslint-disable-line no-extra-parens
+
+
+
+ );
+
+ return (
+
+ );
+ }
+
+ /**
+ * Renders the loading indicator.
+ *
+ * @returns {ReactElement|null}
+ */
+ _renderLoadingIndicator() {
+ if (!(this.state.loading
+ && !this.state.items.length
+ && this.state.filterValue.length)) {
+ return null;
+ }
+
+ const content = ( // eslint-disable-line no-extra-parens
+
+
+
+ );
+
+ return (
+
+ );
+ }
+
+ /**
+ * Sends a query to the resourceClient.
+ *
+ * @param {string} filterValue - The string to use for the search.
+ * @returns {void}
+ */
+ _sendQuery(filterValue) {
+ if (!filterValue) {
+ return;
+ }
+
+ this.setState({
+ loading: true,
+ error: false
+ });
+
+ const resourceClient = this.props.resourceClient || {
+ makeQuery: () => Promise.resolve([]),
+ parseResults: results => results
+ };
+
+ resourceClient.makeQuery(filterValue)
+ .then(results => {
+ if (this.state.filterValue !== filterValue) {
+ this.setState({
+ loading: false,
+ error: false
+ });
+
+ return;
+ }
+ const itemGroups = [
+ {
+ items: resourceClient.parseResults(results)
+ }
+ ];
+
+ this.setState({
+ items: itemGroups,
+ isOpen: true,
+ loading: false,
+ error: false
+ });
+ })
+ .catch(() => {
+ this.setState({
+ error: true,
+ loading: false,
+ isOpen: false
+ });
+ });
+ }
+}
+
+export default MultiSelectAutocomplete;
diff --git a/react/features/base/react/components/web/index.js b/react/features/base/react/components/web/index.js
index e7b01210d..d1f031695 100644
--- a/react/features/base/react/components/web/index.js
+++ b/react/features/base/react/components/web/index.js
@@ -1,3 +1,4 @@
export { default as Container } from './Container';
export { default as Text } from './Text';
export { default as Watermarks } from './Watermarks';
+export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete';
diff --git a/react/features/device-selection/components/DeviceSelector.web.js b/react/features/device-selection/components/DeviceSelector.web.js
index 0d5517a39..ce7440d52 100644
--- a/react/features/device-selection/components/DeviceSelector.web.js
+++ b/react/features/device-selection/components/DeviceSelector.web.js
@@ -4,8 +4,6 @@ import React, { Component } from 'react';
import { translate } from '../../base/i18n';
-const EXPAND_ICON = ;
-
/**
* React component for selecting a device from a select element. Wraps
* AKDropdownMenu with device selection specific logic.
@@ -117,7 +115,9 @@ class DeviceSelector extends Component {
{ triggerText }
- { EXPAND_ICON }
+
);
}
diff --git a/react/features/dial-out/components/DialOutDialog.web.js b/react/features/dial-out/components/DialOutDialog.web.js
index 95e7b2563..83d07d289 100644
--- a/react/features/dial-out/components/DialOutDialog.web.js
+++ b/react/features/dial-out/components/DialOutDialog.web.js
@@ -109,7 +109,7 @@ class DialOutDialog extends Component {
/**
* Renders the dialog content.
*
- * @returns {XML}
+ * @returns {ReactElement}
* @private
*/
_renderContent() {
@@ -127,7 +127,7 @@ class DialOutDialog extends Component {
* Renders the error message to display if the dial phone number is not
* allowed.
*
- * @returns {XML}
+ * @returns {ReactElement}
* @private
*/
_renderErrorMessage() {
diff --git a/react/features/invite/actions.js b/react/features/invite/actions.js
index 65277de33..f36bf71b1 100644
--- a/react/features/invite/actions.js
+++ b/react/features/invite/actions.js
@@ -4,7 +4,7 @@ import {
UPDATE_DIAL_IN_NUMBERS_FAILED,
UPDATE_DIAL_IN_NUMBERS_SUCCESS
} from './actionTypes';
-import { InviteDialog } from './components';
+import { AddPeopleDialog, InviteDialog } from './components';
declare var $: Function;
declare var APP: Object;
@@ -18,6 +18,15 @@ export function openInviteDialog() {
return openDialog(InviteDialog);
}
+/**
+ * Opens the Add People Dialog.
+ *
+ * @returns {Function}
+ */
+export function openAddPeopleDialog() {
+ return openDialog(AddPeopleDialog);
+}
+
/**
* Sends AJAX requests for dial-in numbers and conference ID.
*
diff --git a/react/features/invite/components/AddPeopleDialog.native.js b/react/features/invite/components/AddPeopleDialog.native.js
new file mode 100644
index 000000000..14532cc1c
--- /dev/null
+++ b/react/features/invite/components/AddPeopleDialog.native.js
@@ -0,0 +1,3 @@
+/**
+ * Created by ystamcheva on 8/6/17.
+ */
diff --git a/react/features/invite/components/AddPeopleDialog.web.js b/react/features/invite/components/AddPeopleDialog.web.js
new file mode 100644
index 000000000..d83da3f0a
--- /dev/null
+++ b/react/features/invite/components/AddPeopleDialog.web.js
@@ -0,0 +1,259 @@
+import React, { Component } from 'react';
+import { Immutable } from 'nuclear-js';
+import { connect } from 'react-redux';
+import Avatar from '@atlaskit/avatar';
+
+import { getInviteURL } from '../../base/connection';
+import { Dialog } from '../../base/dialog';
+import { translate } from '../../base/i18n';
+import MultiSelectAutocomplete
+ from '../../base/react/components/web/MultiSelectAutocomplete';
+
+import { invitePeople, searchPeople } from '../functions';
+
+/**
+ * The dialog that allows to invite people to the call.
+ */
+class AddPeopleDialog extends Component {
+ /**
+ * {@code AddPeopleDialog}'s property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * The URL pointing to the service allowing for people invite.
+ */
+ _inviteServiceUrl: React.PropTypes.string,
+
+ /**
+ * The url of the conference to invite people to.
+ */
+ _inviteUrl: React.PropTypes.string,
+
+ /**
+ * The JWT token.
+ */
+ _jwt: React.PropTypes.string,
+
+ /**
+ * The URL pointing to the service allowing for people search.
+ */
+ _peopleSearchUrl: React.PropTypes.string,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: React.PropTypes.func
+ };
+
+ /**
+ * Initializes a new {@code AddPeopleDialog} instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ /**
+ * Indicating that an error occurred when adding people to the call.
+ */
+ addToCallError: false,
+
+ /**
+ * Indicating that we're currently adding the new people to the
+ * call.
+ */
+ addToCallInProgress: false,
+
+ /**
+ * The list of invite items.
+ */
+ inviteItems: new Immutable.List()
+ };
+
+ this._multiselect = null;
+ this._resourceClient = {
+ makeQuery: text => searchPeople(
+ this.props._peopleSearchUrl, this.props._jwt, text),
+ parseResults: response => response.map(user => {
+ const avatar = ( // eslint-disable-line no-extra-parens
+
+ );
+
+ return {
+ content: user.name,
+ value: user.id,
+ elemBefore: avatar,
+ item: user
+ };
+ })
+ };
+
+ this._isAddDisabled = this._isAddDisabled.bind(this);
+ this._onSelectionChange = this._onSelectionChange.bind(this);
+ this._onSubmit = this._onSubmit.bind(this);
+ this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
+ }
+
+ /**
+ * React Component method that executes once component is updated.
+ *
+ * @param {Object} prevState - The state object before the update.
+ * @returns {void}
+ */
+ componentDidUpdate(prevState) {
+ /**
+ * Clears selected items from the multi select component on successful
+ * invite.
+ */
+ if (prevState.addToCallError
+ && !this.state.addToCallInProgress
+ && !this.state.addToCallError
+ && this._multiselect) {
+ this._multiselect.clear();
+ }
+ }
+
+ /**
+ * Renders the content of this component.
+ *
+ * @returns {ReactElement}
+ */
+ render() {
+ return (
+
+ );
+ }
+
+ /**
+ * Renders the input form.
+ *
+ * @returns {ReactElement}
+ * @private
+ */
+ _getUserInputForm() {
+ const { t } = this.props;
+
+ return (
+
+
+
+ );
+ }
+
+ /**
+ * Indicates if the Add button should be disabled.
+ *
+ * @returns {boolean} - True to indicate that the Add button should
+ * be disabled, false otherwise.
+ * @private
+ */
+ _isAddDisabled() {
+ return !this.state.inviteItems.length
+ || this.state.addToCallInProgress;
+ }
+
+ /**
+ * Handles a selection change.
+ *
+ * @param {Map} selectedItems - The list of selected items.
+ * @private
+ * @returns {void}
+ */
+ _onSelectionChange(selectedItems) {
+ const selectedIds = selectedItems.map(o => o.item);
+
+ this.setState({
+ inviteItems: selectedIds
+ });
+ }
+
+ /**
+ * Handles the submit button action.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onSubmit() {
+ if (!this._isAddDisabled()) {
+ this.setState({
+ addToCallInProgress: true
+ });
+
+ invitePeople(
+ this.props._inviteServiceUrl,
+ this.props._inviteUrl,
+ this.props._jwt,
+ this.state.inviteItems)
+ .then(() => {
+ this.setState({
+ addToCallInProgress: false
+ });
+ })
+ .catch(() => {
+ this.setState({
+ addToCallInProgress: false,
+ addToCallError: true
+ });
+ });
+ }
+ }
+
+ /**
+ * Sets the instance variable for the multi select component
+ * element so it can be accessed directly.
+ *
+ * @param {Object} element - The DOM element for the component's dialog.
+ * @private
+ * @returns {void}
+ */
+ _setMultiSelectElement(element) {
+ this._multiselect = element;
+ }
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated
+ * {@code AddPeopleDialog}'s props.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * _peopleSearchUrl: React.PropTypes.string,
+ * _jwt: React.PropTypes.string
+ * }}
+ */
+function _mapStateToProps(state) {
+ const { peopleSearchUrl, inviteServiceUrl } = state['features/base/config'];
+
+ return {
+ _jwt: state['features/jwt'].jwt,
+ _inviteUrl: getInviteURL(state),
+ _inviteServiceUrl: inviteServiceUrl,
+ _peopleSearchUrl: peopleSearchUrl
+ };
+}
+
+export default translate(
+ connect(_mapStateToProps)(AddPeopleDialog));
diff --git a/react/features/invite/components/index.js b/react/features/invite/components/index.js
index 36cdc6dc8..6c1bb18f5 100644
--- a/react/features/invite/components/index.js
+++ b/react/features/invite/components/index.js
@@ -1 +1,2 @@
export { default as InviteDialog } from './InviteDialog';
+export { default as AddPeopleDialog } from './AddPeopleDialog';
diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js
new file mode 100644
index 000000000..a96829565
--- /dev/null
+++ b/react/features/invite/functions.js
@@ -0,0 +1,46 @@
+declare var $: Function;
+
+/**
+ * Sends an ajax request to a directory service.
+ *
+ * @param {string} serviceUrl - The service to query.
+ * @param {string} jwt - The jwt token to pass to the search service.
+ * @param {string} text - Text to search.
+ * @returns {Promise} - The promise created by the request.
+ */
+export function searchPeople(serviceUrl, jwt, text) {
+ const queryTypes = '["conferenceRooms","user","room"]';
+
+ return new Promise((resolve, reject) => {
+ $.getJSON(`${serviceUrl}?query=${encodeURIComponent(text)}
+ &queryTypes=${queryTypes}&jwt=${jwt}`,
+ response => resolve(response)
+ ).fail((jqxhr, textStatus, error) =>
+ reject(error)
+ );
+ });
+}
+
+/**
+ * Sends a post request to an invite service.
+ *
+ * @param {string} inviteServiceUrl - The invite service that generates the
+ * invitation.
+ * @param {string} inviteUrl - The url to the conference.
+ * @param {string} jwt - The jwt token to pass to the search service.
+ * @param {Immutable.List} inviteItems - The list of items to invite.
+ * @returns {Promise} - The promise created by the request.
+ */
+export function invitePeople(inviteServiceUrl, inviteUrl, jwt, inviteItems) { // eslint-disable-line max-params, max-len
+ return new Promise((resolve, reject) => {
+ $.post(`${inviteServiceUrl}?token=${jwt}`,
+ JSON.stringify({
+ 'invited': inviteItems,
+ 'url': inviteUrl }),
+ response => resolve(response),
+ 'json')
+ .fail((jqxhr, textStatus, error) =>
+ reject(error)
+ );
+ });
+}
diff --git a/react/features/jwt/middleware.js b/react/features/jwt/middleware.js
index e719fdf86..7b0ab5dd2 100644
--- a/react/features/jwt/middleware.js
+++ b/react/features/jwt/middleware.js
@@ -180,6 +180,7 @@ function _setJWT(store, next, action) {
if (jwtPayload) {
const { context, iss } = jwtPayload;
+ action.jwt = jwt;
action.issuer = iss;
if (context) {
action.callee = context.callee;
diff --git a/react/features/toolbox/defaultToolbarButtons.js b/react/features/toolbox/defaultToolbarButtons.js
index 4e46d807e..a934846cd 100644
--- a/react/features/toolbox/defaultToolbarButtons.js
+++ b/react/features/toolbox/defaultToolbarButtons.js
@@ -4,7 +4,7 @@ import React from 'react';
import { openDeviceSelectionDialog } from '../device-selection';
import { openDialOutDialog } from '../dial-out';
-import { openInviteDialog } from '../invite';
+import { openAddPeopleDialog, openInviteDialog } from '../invite';
import UIEvents from '../../../service/UI/UIEvents';
declare var APP: Object;
@@ -15,6 +15,19 @@ declare var JitsiMeetJS: Object;
* All toolbar buttons' descriptors.
*/
const buttons: Object = {
+ addtocall: {
+ classNames: [ 'button', 'icon-add' ],
+ enabled: true,
+ id: 'toolbar_button_add',
+ isDisplayed: () => !APP.store.getState()['features/jwt'].isGuest,
+ onClick() {
+ JitsiMeetJS.analytics.sendEvent('toolbar.add.clicked');
+
+ return openAddPeopleDialog();
+ },
+ tooltipKey: 'toolbar.addPeople'
+ },
+
/**
* The descriptor of the camera toolbar button.
*/