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 +
+ { t('inlineDialogFailure.supportMsg') } + + + { t('inlineDialogFailure.support') } + + + . +
+ ) + : 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 ( + + { this._getUserInputForm() } + + ); + } + + /** + * 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. */