From 9ba3a1c4ff1ed0bc02fa2f55f763d19e3f46a843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Wed, 5 Apr 2017 17:14:26 +0200 Subject: [PATCH] feat(conference): add audio only mode Audio only mode can be used to save bandwidth. In this mode local video is muted and last N is set to 0, thus disabling all remote video. When this mode is enabled avatars are shown. --- conference.js | 75 ++- css/_font.scss | 90 +-- css/_toolbars.scss | 6 +- css/_videolayout_default.scss | 20 +- fonts/jitsi.eot | Bin 6724 -> 7316 bytes fonts/jitsi.svg | 4 +- fonts/jitsi.ttf | Bin 6568 -> 7160 bytes fonts/jitsi.woff | Bin 6644 -> 7236 bytes fonts/selection.json | 520 ++++++++++-------- interface_config.js | 2 +- lang/main.json | 6 + modules/UI/UI.js | 8 + modules/UI/videolayout/LargeVideoManager.js | 27 +- modules/UI/videolayout/RemoteVideo.js | 1 + modules/UI/videolayout/SmallVideo.js | 4 +- modules/UI/videolayout/VideoLayout.js | 18 + react/features/base/conference/middleware.js | 8 + .../conference/components/Conference.web.js | 2 + .../status-label/components/AudioOnlyLabel.js | 104 ++++ .../status-label/components/StatusLabel.js | 58 ++ .../features/status-label/components/index.js | 1 + react/features/status-label/index.js | 1 + .../toolbox/components/AudioOnlyButton.js | 103 ++++ .../toolbox/components/Toolbar.web.js | 11 + .../toolbox/components/ToolbarButton.web.js | 2 +- react/features/toolbox/components/index.js | 1 + .../features/toolbox/defaultToolbarButtons.js | 32 ++ service/UI/UIEvents.js | 5 + 28 files changed, 836 insertions(+), 273 deletions(-) mode change 100644 => 100755 fonts/selection.json create mode 100644 react/features/status-label/components/AudioOnlyLabel.js create mode 100644 react/features/status-label/components/StatusLabel.js create mode 100644 react/features/status-label/components/index.js create mode 100644 react/features/status-label/index.js create mode 100644 react/features/toolbox/components/AudioOnlyButton.js diff --git a/conference.js b/conference.js index 92ab12ef2..6065351d3 100644 --- a/conference.js +++ b/conference.js @@ -1085,6 +1085,43 @@ export default { }); }, + /** + * Triggers a tooltip to display when a feature was attempted to be used + * while in audio only mode. + * + * @param {string} featureName - The name of the feature that attempted to + * toggle. + * @private + * @returns {void} + */ + _displayAudioOnlyTooltip(featureName) { + let tooltipElementId = null; + + switch (featureName) { + case 'screenShare': + tooltipElementId = '#screenshareWhileAudioOnly'; + break; + case 'videoMute': + tooltipElementId = '#unmuteWhileAudioOnly'; + break; + } + + if (tooltipElementId) { + APP.UI.showToolbar(6000); + APP.UI.showCustomToolbarPopup( + tooltipElementId, true, 5000); + } + }, + + /** + * Returns whether or not the conference is currently in audio only mode. + * + * @returns {boolean} + */ + isAudioOnly() { + return Boolean( + APP.store.getState()['features/base/conference'].audioOnly); + }, videoSwitchInProgress: false, toggleScreenSharing(shareScreen = !this.isSharingScreen) { @@ -1097,6 +1134,11 @@ export default { return; } + if (this.isAudioOnly()) { + this._displayAudioOnlyTooltip('screenShare'); + return; + } + this.videoSwitchInProgress = true; let externalInstallation = false; @@ -1400,6 +1442,10 @@ export default { } }); + APP.UI.addListener( + UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY, + () => this._displayAudioOnlyTooltip('videoMute')); + APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => { let smallVideoId = smallVideo.getId(); @@ -1512,7 +1558,14 @@ export default { }); APP.UI.addListener(UIEvents.AUDIO_MUTED, muteLocalAudio); - APP.UI.addListener(UIEvents.VIDEO_MUTED, muteLocalVideo); + APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => { + if (this.isAudioOnly() && !muted) { + this._displayAudioOnlyTooltip('videoMute'); + return; + } + + muteLocalVideo(muted); + }); room.on(ConnectionQualityEvents.LOCAL_STATS_UPDATED, (stats) => { @@ -1661,6 +1714,14 @@ export default { micDeviceId: null }) .then(([stream]) => { + if (this.isAudioOnly()) { + return stream.mute() + .then(() => stream); + } + + return stream; + }) + .then(stream => { this.useVideoStream(stream); logger.log('switched local video device'); APP.settings.setCameraDeviceId(cameraDeviceId, true); @@ -1707,6 +1768,18 @@ export default { } ); + APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => { + muteLocalVideo(audioOnly); + + // Immediately update the UI by having remote videos and the large + // video update themselves instead of waiting for some other event + // to cause the update, usually PARTICIPANT_CONN_STATUS_CHANGED. + // There is no guarantee another event will trigger the update + // immediately and in all situations, for example because a remote + // participant is having connection trouble so no status changes. + APP.UI.updateAllVideos(); + }); + APP.UI.addListener( UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this) ); diff --git a/css/_font.scss b/css/_font.scss index 7fffde3c8..3522f50a0 100644 --- a/css/_font.scss +++ b/css/_font.scss @@ -26,119 +26,125 @@ } .icon-mic-camera-combined:before { - content: "\e903"; + content: "\e903"; } .icon-feedback:before { - content: "\e91d"; + content: "\e91d"; } .icon-toggle-filmstrip:before { - content: "\e91c"; + content: "\e91c"; } .icon-avatar:before { - content: "\e901"; + content: "\e901"; } .icon-hangup:before { - content: "\e905"; + content: "\e905"; } .icon-chat:before { - content: "\e906"; + content: "\e906"; } .icon-download:before { - content: "\e902"; -} -.icon-dialpad:before { - content: "\e61c"; + content: "\e902"; } .icon-edit:before { - content: "\e907"; + content: "\e907"; } .icon-share-doc:before { - content: "\e908"; + content: "\e908"; } .icon-telephone:before { - content: "\e909"; + content: "\e909"; } .icon-kick:before { - content: "\e904"; + content: "\e904"; } .icon-menu-up:before { - content: "\e91f"; + content: "\e91f"; } .icon-menu-down:before { - content: "\e920"; + content: "\e920"; } .icon-full-screen:before { - content: "\e90b"; + content: "\e90b"; } .icon-exit-full-screen:before { - content: "\e90c"; + content: "\e90c"; } .icon-star-full:before { - content: "\e90a"; + content: "\e90a"; } .icon-security:before { - content: "\e90d"; + content: "\e90d"; } .icon-security-locked:before { - content: "\e90e"; + content: "\e90e"; } .icon-reload:before { - content: "\e90f"; + content: "\e90f"; } .icon-microphone:before { - content: "\e910"; + content: "\e910"; } .icon-mic-empty:before { - content: "\e911"; + content: "\e911"; } .icon-mic-disabled:before { - content: "\e912"; + content: "\e912"; } .icon-raised-hand:before { - content: "\e91e"; + content: "\e91e"; } .icon-contactList:before { - content: "\e91b"; + content: "\e91b"; } .icon-link:before { - content: "\e913"; + content: "\e913"; } .icon-shared-video:before { - content: "\e914"; + content: "\e914"; } .icon-settings:before { - content: "\e915"; + content: "\e915"; } .icon-star:before { - content: "\e916"; + content: "\e916"; } .icon-switch-camera:before { - content: "\e921"; + content: "\e921"; } .icon-share-desktop:before { - content: "\e917"; + content: "\e917"; } .icon-camera:before { - content: "\e918"; + content: "\e918"; } .icon-camera-disabled:before { - content: "\e919"; + content: "\e919"; } .icon-volume:before { - content: "\e91a"; + content: "\e91a"; } .icon-connection-lost:before { - content: "\e900"; + content: "\e900"; } .icon-connection:before { - content: "\e61a"; + content: "\e61a"; } .icon-recDisable:before { - content: "\e613"; + content: "\e613"; } .icon-recEnable:before { - content: "\e614"; + content: "\e614"; } .icon-presentation:before { - content: "\e603"; -} \ No newline at end of file + content: "\e603"; +} +.icon-dialpad:before { + content: "\e925"; +} +.icon-visibility:before { + content: "\e923"; +} +.icon-visibility-off:before { + content: "\e924"; +} diff --git a/css/_toolbars.scss b/css/_toolbars.scss index 999fc6a0f..d73f5b635 100644 --- a/css/_toolbars.scss +++ b/css/_toolbars.scss @@ -71,6 +71,10 @@ &.icon-microphone { @extend .icon-mic-disabled; } + + &.icon-visibility { + @extend .icon-visibility-off; + } } &.unclickable { @@ -170,7 +174,7 @@ width: $defaultToolbarSize; -webkit-transform: translateX(-100%); - .button.toggled:not(.icon-raised-hand) { + .button.toggled:not(.icon-raised-hand):not(.button-active) { background: $toolbarSelectBackground; cursor: pointer; text-decoration: none; diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index dc3cbc4fd..5c3faa545 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -115,6 +115,12 @@ visibility: hidden; z-index: $zindex2; } + + &.audio-only { + .videoThumbnailProblemFilter { + filter: none; + } + } } #localVideoWrapper { @@ -489,19 +495,31 @@ 0px 0px 1px rgba(0,0,0,0.3); } +.audio-only-label { + cursor: default; + display: flex; + height: auto; + justify-content: center; + z-index: $centeredVideoLabelZ; +} + +.audio-only-label, .video-state-indicator { background: $videoStateIndicatorBackground; color: $videoStateIndicatorColor; font-size: 13px; + height: 40px; line-height: 20px; text-align: center; min-width: 40px; - height: 40px; padding: 10px 5px; border-radius: 50%; position: absolute; box-sizing: border-box; } +.video-state-indicator { + height: 40px; +} #videoResolutionLabel, .centeredVideoLabel { diff --git a/fonts/jitsi.eot b/fonts/jitsi.eot index 29935c6107555fe8922d435ff4a7265c09e34185..abce56b5fe1676445aaf3cbce46d3964c197f486 100755 GIT binary patch delta 1187 zcmZ8fT}YEr7=F(=-`_rME9cyg({h`w%uU;`Ein@@NfG-qiir-hB=ZNdKQg{d zB#H_mtiZY`6oTkNBD(4-O1h{U17T1%VG+_?=X`U2e(-YM=XsxVzUMvfdFPwlAxv5U z{+33C0GE(m(P-nyhByGcq0!qn6d8@*yI&ohvnsES_=f?|9ROqVK!42UQ;_y)9{3z7H41lGT;!VSm zq5f~TE4|$uZUMB6Uih~z@|mAs9&_i zA#PVWl>BZ%=3nNYM#(7BQ&4QXML*-hx44a;pQpr7EZA+E z{l0+S(}4XxH@4ff*ic-K^`3HU)DpYRjhzhygRi~K&!V~`WV3}js?%%d#Kmr_wfo{k zXC}j!Q&M01%5;uYQTO1InnP#yw46OulU@NhYOITVBA0=OQmBDF)SA~*QidwEhhhX7ae8(K(l&1t17FL&J|&>`Mma=pqHFad|SntLL$wq zRb~`qK7C{o(ivS9_24{8WzfO2Xyti2Lz#`)ERo^Zm7-KcU2>vFSN|IdIL5-~jm!J8 zYw{mI#bJsizz{oO+*096$(V$_I~t9ExiQdlXQ7Fo0xmXdyr$4X)oM(uN*Riw9Kxs? zTW~XeO@M^R4f2pIaV9Rro#ftg3%tbF^5gsmp-^ZMCWQsjBpwwPB&}2@9hI(1bDDZh kujaO9QS(H|%+G|MNdd(ez+#O5A4@Sxev*7E z`9_=*XbR)xMj7GFx=a&T>Ny#_82FiYFc&hgFmN*PG6*q_VF)#=liz=Hc z!U$Fv10uwT!u9&_fzcj<|2AP~GZ#JqfoL%Kz6ZH@}lw#s~lpXP^fF diff --git a/fonts/jitsi.svg b/fonts/jitsi.svg index 0047c8d29..e5d2f7728 100755 --- a/fonts/jitsi.svg +++ b/fonts/jitsi.svg @@ -11,7 +11,6 @@ - @@ -46,4 +45,7 @@ + + + \ No newline at end of file diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf index 01293b52075b33e2defe204a554f4861ad5ef0b3..06d7b3b82ebdd8fc84292a0fa0f81e6dd003a0df 100755 GIT binary patch delta 1199 zcmZ8gT}V_x6h1R^_wL=*?uP5SKmL@vYg?wfvcI;8R)|VU*^fq|*ix2c{(x4B#`UF$ zNKiLIs30OMq=&*Fh#sVeUh1VN>7iZ>goSzu3z63C%+=l11`g+Z-#Ih)J9Fkf@hyiG zAOMg7qrjl-@G*~9{vgJXd5w1Jba$jLcIR#h0Leodu0BHI z2NJE_k-Mw8bFiP&tzLZX?!T)}Lm{ti>Zpbi!(!ogm}F?6kOKP#6@$9GhqR z@GiE24?pJ3F=dMm`;LG=X!KUnfZt6WcA1*83#idsK+Q69*xl4s$td_6>I2+L4u$RZ z@S&2VZ=4)%%gbvUo@`7-babuM->^Efp;S@#&>T<|3?R zis^tTOa2B*S3d^nQqWoF+}-9?Ra=h+^221zY{aR=j>L1&FKD~mT$)m5BdaYB+>4F(P?I(Cg@xlDWupOhYEpjcte zY#y)P3=ZsZFLrqh+Mx?B|38TIPzpBR|C3O1iSG6Bg5y`mnxiBcJIJXy5*G#Qc9#&i@CFZ8yolv`+ zfk8q7Xh~i{esKxVp+F$n0g`86W|5hAz@AZY5@UegGnto)|Ns8~50Yk3c*gup_?Z+? zi~%gh`2VpKqvR*aw~}wfIe|tnPJSpYym=zy1eSVE1}_GF<{iw13@i+s47?0N4B`wj z42leD3|b5f!p5S?riw6v6~=%FF`{t2K73%bhv2_W*xAg5Pe33VOg{N{5JWRR*(}TU zgI^gG4?rxn?c>>aew(ih+$>;^TosG21TjGLBuR5sZHO>f4ot8(Ffg%{FbK1hF*va< zVPIilV(^+AA=!}umgQm)VUS~pVbo!gVsc?>WBSAF#9YICj`uRB_DVIKj!pS;x7E^9Gj&mlc;ER|CkE4B8AE Qfq?}AOyH2rmg;2$0FkGl^Z)<= diff --git a/fonts/jitsi.woff b/fonts/jitsi.woff index 7934afb8c71d45c75c08db880720e8283045fe5d..ef4c204fa36736c5c1b61d82c6d76dd77e0069ce 100755 GIT binary patch delta 1255 zcmZ8fT}YEr7=F(=-}mj?v`w7b+?tl#Y`S#H{7Hmw?2-T8+0W%}SzoBz6*j3?e_qk+}b$Y9Mxsb6+r*S5Yu9eEt}6 z-vKPm9B&Q;@B#}Fcxr;;lRGb+c89RsEWz1i5yzD3#8a~mB zx;9%q5>XE2&y{B*v2*ya?GEOfRn)`OXl$e(IlM?lIp%I%e17u8DKu;652SSrBPQ75 z`D6zvkGGO`Z=x(wo=T=tcz~h>wqRVaB_QEcIg z_K95r5jH{%>_TgPpSgsT>k0Sb_l4@*dXhF;N9u^iOVpfuBXwRPIwTE7e$_MKAuok2 zOK9PpPR~tPYZX(Iq?ZG&l&;K#=wisV+qJdFr@FT850-?h7v;pE%!RFWyr- zuF>L6UX6UrRAb{6Z^&yRtR<*1NW>dT=7(GnS79M9WXF)Fq~V|}M-2v^HuP7H7rIUS z3ZIk?Cc&eyE;fs;w}2CC+>TWqfFm#jXa65WZYX8z29)ESYj~oL58H*KTtCp9-pEgt z)k&`vQE&wO&NaX$uP5eK^Xx$~!~CzzD%fo1lPSnVxGU<1*HJ1%Ix?b_7nz7;FXp0T zmg6lYY22>XC65>yenXzXS@^tZd0%c%{S}`G6muCU#4VPJvg)*qOZb0>*a=h^1EX*T zn#3m{#QQAQ6g=>(Vp3J=pa@DKN~)-hHq*BZSeV^lkJzG6EQE#Q!Uthql*L+cT>L2I zOA+agG_NVvbZh2iy<9JM%U9)TZG-lx_O5n8yQIt4HR`VGmh&E~RaC<({6YBQPWhTT ITzrrG1JWh>oB#j- delta 614 zcmX?N@x@rI+~3WOfsp|SB)>3lgXt9vjFSyzgePi>)jv85qQ6 zfP57Y=3L5pAtSe>0x0GHiopt00kmIxa3vUwcNxCpv4j{K)wPP zn=)+9OUz9LiUBQR%md-O6KaPGF9v?*9n6Ic zEDW3sybMAN;tVnjiVSKDS_}-r#-hrmiZFr|#()ShqHw)Fd|7jfR;(%`b<^5bd%IgCM@ NVIu?Q=6O=f7y(B}qPYM7 diff --git a/fonts/selection.json b/fonts/selection.json old mode 100644 new mode 100755 index 138f42f95..52512aec2 --- a/fonts/selection.json +++ b/fonts/selection.json @@ -11,10 +11,10 @@ ], "isMulticolor": false, "isMulticolor2": false, - "grid": 0, "tags": [ "Combined Shape" - ] + ], + "grid": 0 }, "attrs": [ {} @@ -27,7 +27,7 @@ "code": 59651 }, "setIdx": 0, - "setId": 2, + "setId": 3, "iconIdx": 0 }, { @@ -40,24 +40,24 @@ ], "isMulticolor": false, "isMulticolor2": false, - "grid": 0, "tags": [ "ic_thumb_up_black_24px" - ] + ], + "grid": 0 }, "attrs": [ {} ], "properties": { "order": 104, - "id": 847, + "id": 1, "name": "feedback", "prevSize": 32, "code": 59677 }, - "setIdx": 1, - "setId": 1, - "iconIdx": 0 + "setIdx": 0, + "setId": 3, + "iconIdx": 1 }, { "icon": { @@ -69,804 +69,804 @@ ], "isMulticolor": false, "isMulticolor2": false, - "grid": 0, "tags": [ "ic_call_to_action_black_24px" - ] + ], + "grid": 0 }, "attrs": [ {} ], "properties": { "order": 103, - "id": 846, + "id": 2, "name": "toggle-filmstrip", "prevSize": 32, "code": 59676 }, - "setIdx": 1, - "setId": 1, - "iconIdx": 1 + "setIdx": 0, + "setId": 3, + "iconIdx": 2 }, { "icon": { "paths": [ "M512 820c106 0 200-56 256-138-2-84-172-132-256-132-86 0-254 48-256 132 56 82 150 138 256 138zM512 214c-70 0-128 58-128 128s58 128 128 128 128-58 128-128-58-128-128-128zM512 86c236 0 426 190 426 426s-190 426-426 426-426-190-426-426 190-426 426-426z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "account_circle" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 11, + "id": 3, "order": 60, "ligatures": "account_circle", "prevSize": 32, "code": 59649, "name": "avatar" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 2 + "setIdx": 0, + "setId": 3, + "iconIdx": 3 }, { "icon": { "paths": [ "M512 384c-68 0-134 10-196 30v132c0 16-10 34-24 40-42 20-80 46-114 78-8 8-18 12-30 12s-22-4-30-12l-106-106c-8-8-12-18-12-30s4-22 12-30c130-124 306-200 500-200s370 76 500 200c8 8 12 18 12 30s-4 22-12 30l-106 106c-8 8-18 12-30 12s-22-4-30-12c-34-32-72-58-114-78-14-6-24-20-24-38v-132c-62-20-128-32-196-32z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "call_end" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 122, + "id": 4, "order": 63, "ligatures": "call_end", "prevSize": 32, "code": 59653, "name": "hangup" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 3 + "setIdx": 0, + "setId": 3, + "iconIdx": 4 }, { "icon": { "paths": [ "M854 682v-512h-684v598l86-86h598zM854 86c46 0 84 38 84 84v512c0 46-38 86-84 86h-598l-170 170v-768c0-46 38-84 84-84h684z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "chat_bubble_outline" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 148, + "id": 5, "order": 61, "ligatures": "chat_bubble_outline", "prevSize": 32, "code": 59654, "name": "chat" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 4 + "setIdx": 0, + "setId": 3, + "iconIdx": 5 }, { "icon": { "paths": [ "M726 554h-128v-170h-172v170h-128l214 214zM826 428c110 8 198 100 198 212 0 118-96 214-214 214h-554c-142 0-256-114-256-256 0-132 100-240 228-254 54-102 160-174 284-174 156 0 284 110 314 258z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "cloud_download" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 164, + "id": 6, "order": 99, "ligatures": "cloud_download", "prevSize": 32, "code": 59650, "name": "download" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 5 + "setIdx": 0, + "setId": 3, + "iconIdx": 6 }, { "icon": { "paths": [ "M884 300l-78 78-160-160 78-78c16-16 44-16 60 0l100 100c16 16 16 44 0 60zM128 736l472-472 160 160-472 472h-160v-160z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "mode_edit" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 185, + "id": 7, "order": 89, "ligatures": "create, edit, mode_edit", "prevSize": 32, "code": 59655, "name": "edit" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 6 + "setIdx": 0, + "setId": 3, + "iconIdx": 7 }, { "icon": { "paths": [ "M554 384h236l-236-234v234zM682 598v-86h-340v86h340zM682 768v-86h-340v86h340zM598 86l256 256v512c0 46-40 84-86 84h-512c-46 0-86-38-86-84l2-684c0-46 38-84 84-84h342z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "description" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 206, + "id": 8, "order": 85, "ligatures": "description", "prevSize": 32, "code": 59656, "name": "share-doc" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 7 + "setIdx": 0, + "setId": 3, + "iconIdx": 8 }, { "icon": { "paths": [ "M854 662c24 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 44l-94 94c62 122 162 220 282 282l94-94c12-12 30-14 44-10 48 16 98 24 152 24zM854 214v-44h-44v44h44zM768 128h128v128h-86v86h-42v-214zM640 214v128h-128v-44h86v-42h-86v-128h128v42h-86v44h86zM726 128v214h-44v-214h44z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "dialer_sip" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 215, + "id": 9, "order": 95, "ligatures": "dialer_sip", "prevSize": 32, "code": 59657, "name": "telephone" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 8 + "setIdx": 0, + "setId": 3, + "iconIdx": 9 }, { "icon": { "paths": [ "M512 214l284 426h-568zM214 726h596v84h-596v-84z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "eject" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 242, + "id": 10, "order": 98, "ligatures": "eject", "prevSize": 32, "code": 59652, "name": "kick" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 9 + "setIdx": 0, + "setId": 3, + "iconIdx": 10 }, { "icon": { "paths": [ "M512 342l256 256-60 60-196-196-196 196-60-60z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "expand_less" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 256, + "id": 11, "order": 106, "ligatures": "expand_less", "prevSize": 32, "code": 59679, "name": "menu-up" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 10 + "setIdx": 0, + "setId": 3, + "iconIdx": 11 }, { "icon": { "paths": [ "M708 366l60 60-256 256-256-256 60-60 196 196z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "expand_more" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 257, + "id": 12, "order": 107, "ligatures": "expand_more", "prevSize": 32, "code": 59680, "name": "menu-down" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 11 + "setIdx": 0, + "setId": 3, + "iconIdx": 12 }, { "icon": { "paths": [ "M598 214h212v212h-84v-128h-128v-84zM726 726v-128h84v212h-212v-84h128zM214 426v-212h212v84h-128v128h-84zM298 598v128h128v84h-212v-212h84z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "fullscreen" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 350, + "id": 13, "order": 94, "ligatures": "fullscreen", "prevSize": 32, "code": 59659, "name": "full-screen" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 12 + "setIdx": 0, + "setId": 3, + "iconIdx": 13 }, { "icon": { "paths": [ "M682 342h128v84h-212v-212h84v128zM598 810v-212h212v84h-128v128h-84zM342 342v-128h84v212h-212v-84h128zM214 682v-84h212v212h-84v-128h-128z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "fullscreen_exit" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 351, + "id": 14, "order": 92, "ligatures": "fullscreen_exit", "prevSize": 32, "code": 59660, "name": "exit-full-screen" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 13 + "setIdx": 0, + "setId": 3, + "iconIdx": 14 }, { "icon": { "paths": [ "M512 736l-264 160 70-300-232-202 306-26 120-282 120 282 306 26-232 202 70 300z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "star" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 363, + "id": 15, "order": 101, "ligatures": "grade, star", "prevSize": 32, "code": 59658, "name": "star-full" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 14 + "setIdx": 0, + "setId": 3, + "iconIdx": 15 }, { "icon": { "paths": [ "M768 854v-428h-512v428h512zM768 342c46 0 86 38 86 84v428c0 46-40 84-86 84h-512c-46 0-86-38-86-84v-428c0-46 40-84 86-84h388v-86c0-72-60-132-132-132s-132 60-132 132h-82c0-118 96-214 214-214s214 96 214 214v86h42zM512 726c-46 0-86-40-86-86s40-86 86-86 86 40 86 86-40 86-86 86z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "lock_open" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 473, + "id": 16, "order": 66, "ligatures": "lock_open", "prevSize": 32, "code": 59661, "name": "security" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 15 + "setIdx": 0, + "setId": 3, + "iconIdx": 16 }, { "icon": { "paths": [ "M768 854v-428h-512v428h512zM380 256v86h264v-86c0-72-60-132-132-132s-132 60-132 132zM768 342c46 0 86 38 86 84v428c0 46-40 84-86 84h-512c-46 0-86-38-86-84v-428c0-46 40-84 86-84h42v-86c0-118 96-214 214-214s214 96 214 214v86h42zM512 726c-46 0-86-40-86-86s40-86 86-86 86 40 86 86-40 86-86 86z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "lock_outline" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 474, + "id": 17, "order": 65, "ligatures": "lock_outline", "prevSize": 32, "code": 59662, "name": "security-locked" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 16 + "setIdx": 0, + "setId": 3, + "iconIdx": 17 }, { "icon": { "paths": [ "M512 768v-128l170 170-170 172v-128c-188 0-342-154-342-342 0-66 20-130 54-182l62 62c-20 36-30 76-30 120 0 142 114 256 256 256zM512 170c188 0 342 154 342 342 0 66-20 130-54 182l-62-62c20-36 30-76 30-120 0-142-114-256-256-256v128l-170-170 170-172v128z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "sync" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 482, + "id": 18, "order": 67, "ligatures": "loop, sync", "prevSize": 32, "code": 59663, "name": "reload" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 17 + "setIdx": 0, + "setId": 3, + "iconIdx": 18 }, { "icon": { "paths": [ "M738 470h72c0 146-116 266-256 286v140h-84v-140c-140-20-256-140-256-286h72c0 128 108 216 226 216s226-88 226-216zM512 598c-70 0-128-58-128-128v-256c0-70 58-128 128-128s128 58 128 128v256c0 70-58 128-128 128z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "mic" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 492, + "id": 19, "order": 68, "ligatures": "mic", "prevSize": 32, "code": 59664, "name": "microphone" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 18 + "setIdx": 0, + "setId": 3, + "iconIdx": 19 }, { "icon": { "paths": [ "M738 470h72c0 146-116 266-256 286v140h-84v-140c-140-20-256-140-256-286h72c0 128 108 216 226 216s226-88 226-216zM460 210v264c0 28 24 50 52 50s50-22 50-50l2-264c0-28-24-52-52-52s-52 24-52 52zM512 598c-70 0-128-58-128-128v-256c0-70 58-128 128-128s128 58 128 128v256c0 70-58 128-128 128z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "mic_none" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 493, + "id": 20, "order": 69, "ligatures": "mic_none", "prevSize": 32, "code": 59665, "name": "mic-empty" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 19 + "setIdx": 0, + "setId": 3, + "iconIdx": 20 }, { "icon": { "paths": [ "M182 128l714 714-54 54-178-178c-32 20-72 32-110 38v140h-84v-140c-140-20-256-140-256-286h72c0 128 108 216 226 216 34 0 68-8 98-22l-70-70c-8 2-18 4-28 4-70 0-128-58-128-128v-32l-256-256zM640 476l-256-254v-8c0-70 58-128 128-128s128 58 128 128v262zM810 470c0 50-14 98-38 140l-52-54c12-26 18-54 18-86h72z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "mic_off" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 494, + "id": 21, "order": 70, "ligatures": "mic_off", "prevSize": 32, "code": 59666, "name": "mic-disabled" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 20 + "setIdx": 0, + "setId": 3, + "iconIdx": 21 }, { "icon": { "paths": [ "M982 234v620c0 94-78 170-172 170h-310c-46 0-90-18-122-50l-336-342s54-52 56-52c10-8 22-12 34-12 10 0 18 2 26 6 2 0 184 104 184 104v-508c0-36 28-64 64-64s64 28 64 64v300h42v-406c0-36 28-64 64-64s64 28 64 64v406h42v-364c0-36 28-64 64-64s64 28 64 64v364h44v-236c0-36 28-64 64-64s64 28 64 64z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "pan_tool" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 539, + "id": 22, "order": 105, "ligatures": "pan_tool", "prevSize": 32, "code": 59678, "name": "raised-hand" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 21 + "setIdx": 0, + "setId": 3, + "iconIdx": 22 }, { "icon": { "paths": [ "M704 278c-46 0-86 38-86 84s40 86 86 86 86-40 86-86-40-84-86-84zM704 512c-82 0-150-68-150-150s68-148 150-148 150 66 150 148-68 150-150 150zM320 278c-46 0-86 38-86 84s40 86 86 86 86-40 86-86-40-84-86-84zM320 512c-82 0-150-68-150-150s68-148 150-148 150 66 150 148-68 150-150 150zM918 746v-52c0-24-110-76-214-76-46 0-90 12-128 24 14 16 22 32 22 52v52h320zM534 746v-52c0-24-110-76-214-76s-214 52-214 76v52h428zM704 554c92 0 278 48 278 140v116h-940v-116c0-92 186-140 278-140 52 0 130 16 192 44 62-28 140-44 192-44z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "people_outline" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 549, + "id": 23, "order": 100, "ligatures": "people_outline", "prevSize": 32, "code": 59675, "name": "contactList" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 22 + "setIdx": 0, + "setId": 3, + "iconIdx": 23 }, { "icon": { "paths": [ "M640 598c114 0 342 56 342 170v86h-684v-86c0-114 228-170 342-170zM256 426h128v86h-128v128h-86v-128h-128v-86h128v-128h86v128zM640 512c-94 0-170-76-170-170s76-172 170-172 170 78 170 172-76 170-170 170z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "person_add" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 559, + "id": 24, "order": 87, "ligatures": "person_add", "prevSize": 32, "code": 59667, "name": "link" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 23 + "setIdx": 0, + "setId": 3, + "iconIdx": 24 }, { "icon": { "paths": [ "M512 854c188 0 342-154 342-342s-154-342-342-342-342 154-342 342 154 342 342 342zM512 86c236 0 426 190 426 426s-190 426-426 426-426-190-426-426 190-426 426-426zM426 704v-384l256 192z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "play_circle_outline" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 590, + "id": 25, "order": 82, "ligatures": "play_circle_outline", "prevSize": 32, "code": 59668, "name": "shared-video" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 24 + "setIdx": 0, + "setId": 3, + "iconIdx": 25 }, { "icon": { "paths": [ "M512 662c82 0 150-68 150-150s-68-150-150-150-150 68-150 150 68 150 150 150zM830 554l90 70c8 6 10 18 4 28l-86 148c-6 10-16 12-26 8l-106-42c-22 16-46 32-72 42l-16 112c-2 10-10 18-20 18h-172c-10 0-18-8-20-18l-16-112c-26-10-50-24-72-42l-106 42c-10 4-20 2-26-8l-86-148c-6-10-4-22 4-28l90-70c-2-14-2-28-2-42s0-28 2-42l-90-70c-8-6-10-18-4-28l86-148c6-10 16-12 26-8l106 42c22-16 46-32 72-42l16-112c2-10 10-18 20-18h172c10 0 18 8 20 18l16 112c26 10 50 24 72 42l106-42c10-4 20-2 26 8l86 148c6 10 4 22-4 28l-90 70c2 14 2 28 2 42s0 28-2 42z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "settings" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 665, + "id": 26, "order": 81, "ligatures": "settings", "prevSize": 32, "code": 59669, "name": "settings" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 25 + "setIdx": 0, + "setId": 3, + "iconIdx": 26 }, { "icon": { "paths": [ "M512 658l160 96-42-182 142-124-188-16-72-172-72 172-188 16 142 124-42 182zM938 394l-232 202 70 300-264-160-264 160 70-300-232-202 306-26 120-282 120 282z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "star_border" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 717, + "id": 27, "order": 76, "ligatures": "star_border", "prevSize": 32, "code": 59670, "name": "star" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 26 + "setIdx": 0, + "setId": 3, + "iconIdx": 27 }, { "icon": { "paths": [ "M640 662l150-150-150-150v108h-256v-108l-150 150 150 150v-108h256v108zM854 170c46 0 84 40 84 86v512c0 46-38 86-84 86h-684c-46 0-84-40-84-86v-512c0-46 38-86 84-86h136l78-84h256l78 84h136z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "switch_camera" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 741, + "id": 28, "order": 108, "ligatures": "switch_camera", "prevSize": 32, "code": 59681, "name": "switch-camera" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 27 + "setIdx": 0, + "setId": 3, + "iconIdx": 28 }, { "icon": { "paths": [ "M896 726v-512h-768v512h768zM896 128c46 0 86 40 86 86l-2 512c0 46-38 84-84 84h-214v86h-340v-86h-214c-46 0-86-38-86-84v-512c0-46 40-86 86-86h768z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "tv" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 783, + "id": 29, "order": 93, "ligatures": "tv", "prevSize": 32, "code": 59671, "name": "share-desktop" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 28 + "setIdx": 0, + "setId": 3, + "iconIdx": 29 }, { "icon": { "paths": [ "M726 448l170-170v468l-170-170v150c0 24-20 42-44 42h-512c-24 0-42-18-42-42v-428c0-24 18-42 42-42h512c24 0 44 18 44 42v150z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "videocam" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 798, + "id": 30, "order": 77, "ligatures": "videocam", "prevSize": 32, "code": 59672, "name": "camera" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 29 + "setIdx": 0, + "setId": 3, + "iconIdx": 30 }, { "icon": { "paths": [ "M140 86l756 756-54 54-136-136c-6 4-16 8-24 8h-512c-24 0-42-18-42-42v-428c0-24 18-42 42-42h32l-116-116zM896 278v456l-478-478h264c24 0 44 18 44 42v150z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "videocam_off" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 799, + "id": 31, "order": 78, "ligatures": "videocam_off", "prevSize": 32, "code": 59673, "name": "camera-disabled" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 30 + "setIdx": 0, + "setId": 3, + "iconIdx": 31 }, { "icon": { "paths": [ "M598 138c172 38 298 192 298 374s-126 336-298 374v-88c124-36 212-150 212-286s-88-250-212-286v-88zM704 512c0 76-42 140-106 172v-344c64 32 106 96 106 172zM128 384h170l214-214v684l-214-214h-170v-256z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ "volume_up" ], - "grid": 0, - "attrs": [] + "grid": 0 }, "attrs": [], "properties": { - "id": 821, + "id": 32, "order": 79, "ligatures": "volume_up", "prevSize": 32, "code": 59674, "name": "volume" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 31 + "setIdx": 0, + "setId": 3, + "iconIdx": 32 }, { "icon": { @@ -879,6 +879,7 @@ "M1192.868 584.148c-0.009-0-0.020-0-0.031-0-122.247 0-221.351 98.447-221.372 219.896-0 0.007-0 0.014-0 0.021 0 121.467 99.111 219.935 221.372 219.935 0.011 0 0.021-0 0.032-0 122.248-0.014 221.345-98.477 221.345-219.935 0-0.007-0-0.013-0-0.020-0.021-121.441-99.11-219.883-221.345-219.897zM1194.706 651.393c87.601 0.006 158.614 69.787 158.614 155.866 0 0.006-0 0.012-0 0.019-0.022 86.062-71.026 155.822-158.614 155.828-87.588-0.006-158.593-69.766-158.615-155.826-0-0.007-0-0.014-0-0.020 0-86.079 71.013-155.86 158.613-155.866z", "M1286.795 668.318l48.348 52.528-236.375 217.567-48.348-52.528 236.375-217.567z" ], + "width": 1414, "attrs": [ {}, {}, @@ -888,13 +889,12 @@ {}, {} ], - "width": 1414, "isMulticolor": false, "isMulticolor2": false, - "grid": 0, "tags": [ "connection-lost" - ] + ], + "grid": 0 }, "attrs": [ {}, @@ -907,14 +907,14 @@ ], "properties": { "order": 33, - "id": 34, + "id": 33, "name": "connection-lost", "prevSize": 32, "code": 59648 }, - "setIdx": 1, - "setId": 1, - "iconIdx": 32 + "setIdx": 0, + "setId": 3, + "iconIdx": 33 }, { "icon": { @@ -948,6 +948,8 @@ "visibility": false } ], + "isMulticolor": false, + "isMulticolor2": false, "tags": [ "connection-2" ], @@ -977,15 +979,15 @@ ], "properties": { "order": 37, - "id": 31, + "id": 34, "prevSize": 32, "code": 58906, "name": "connection", "ligatures": "" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 33 + "setIdx": 0, + "setId": 3, + "iconIdx": 34 }, { "icon": { @@ -996,6 +998,8 @@ ], "width": 1140, "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, "tags": [ "recDisable" ], @@ -1004,15 +1008,15 @@ "attrs": [], "properties": { "order": 43, - "id": 22, + "id": 35, "prevSize": 32, "code": 58899, "name": "recDisable", "ligatures": "" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 34 + "setIdx": 0, + "setId": 3, + "iconIdx": 35 }, { "icon": { @@ -1024,6 +1028,8 @@ ], "width": 1142, "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, "tags": [ "recEnable" ], @@ -1032,15 +1038,15 @@ "attrs": [], "properties": { "order": 44, - "id": 21, + "id": 36, "prevSize": 32, "code": 58900, "name": "recEnable", "ligatures": "" }, - "setIdx": 1, - "setId": 1, - "iconIdx": 35 + "setIdx": 0, + "setId": 3, + "iconIdx": 36 }, { "icon": { @@ -1052,6 +1058,8 @@ ], "width": 1088, "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, "tags": [ "presentation" ], @@ -1060,15 +1068,93 @@ "attrs": [], "properties": { "order": 53, - "id": 9, + "id": 37, "prevSize": 32, "code": 58883, "name": "presentation", "ligatures": "" }, + "setIdx": 0, + "setId": 3, + "iconIdx": 37 + }, + { + "icon": { + "paths": [ + "M512 42c46 0 86 40 86 86s-40 86-86 86-86-40-86-86 40-86 86-86zM512 298c46 0 86 40 86 86s-40 86-86 86-86-40-86-86 40-86 86-86zM768 298c46 0 86 40 86 86s-40 86-86 86-86-40-86-86 40-86 86-86zM768 554c46 0 86 40 86 86s-40 86-86 86-86-40-86-86 40-86 86-86zM512 554c46 0 86 40 86 86s-40 86-86 86-86-40-86-86 40-86 86-86zM768 214c-46 0-86-40-86-86s40-86 86-86 86 40 86 86-40 86-86 86zM256 554c46 0 86 40 86 86s-40 86-86 86-86-40-86-86 40-86 86-86zM256 298c46 0 86 40 86 86s-40 86-86 86-86-40-86-86 40-86 86-86zM256 42c46 0 86 40 86 86s-40 86-86 86-86-40-86-86 40-86 86-86zM512 810c46 0 86 40 86 86s-40 86-86 86-86-40-86-86 40-86 86-86z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "dialpad" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 115, + "ligatures": "dialpad", + "id": 217, + "prevSize": 32, + "code": 59685, + "name": "dialpad" + }, "setIdx": 1, - "setId": 1, - "iconIdx": 36 + "setId": 2, + "iconIdx": 217 + }, + { + "icon": { + "paths": [ + "M512 384c70 0 128 58 128 128s-58 128-128 128-128-58-128-128 58-128 128-128zM512 726c118 0 214-96 214-214s-96-214-214-214-214 96-214 214 96 214 214 214zM512 192c214 0 396 132 470 320-74 188-256 320-470 320s-396-132-470-320c74-188 256-320 470-320z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "visibility" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 114, + "ligatures": "remove_red_eye, visibility", + "id": 622, + "prevSize": 32, + "code": 59683, + "name": "visibility" + }, + "setIdx": 1, + "setId": 2, + "iconIdx": 622 + }, + { + "icon": { + "paths": [ + "M506 384h6c70 0 128 58 128 128v8zM322 418c-14 28-24 60-24 94 0 118 96 214 214 214 34 0 66-10 94-24l-66-66c-8 2-18 4-28 4-70 0-128-58-128-128 0-10 2-20 4-28zM86 182l54-54 756 756-54 54c-47.968-47.365-96.266-94.401-144-142-58 24-120 36-186 36-214 0-396-132-470-320 34-84 90-156 160-212-39.017-38.983-77.307-78.693-116-118zM512 298c-28 0-54 6-78 16l-92-92c52-20 110-30 170-30 214 0 394 132 468 320-32 80-82 148-146 202l-124-124c10-24 16-50 16-78 0-118-96-214-214-214z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "visibility_off" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 113, + "ligatures": "visibility_off", + "id": 816, + "prevSize": 32, + "code": 59684, + "name": "visibility-off" + }, + "setIdx": 1, + "setId": 2, + "iconIdx": 816 } ], "height": 1024, diff --git a/interface_config.js b/interface_config.js index 5bd0545f4..a9d0a1d59 100644 --- a/interface_config.js +++ b/interface_config.js @@ -38,7 +38,7 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars //main toolbar 'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup', //extended toolbar - 'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'sip', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line + 'profile', 'contacts', 'chat', 'audioonly', 'recording', 'etherpad', 'sharedvideo', 'sip', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line /** * Main Toolbar Buttons * All of them should be in TOOLBAR_BUTTONS diff --git a/lang/main.json b/lang/main.json index c8ada23ec..22bc14cb8 100644 --- a/lang/main.json +++ b/lang/main.json @@ -14,6 +14,11 @@ "defaultNickname": "ex. Jane Pink", "defaultLink": "e.g. __url__", "callingName": "__name__", + "audioOnly": { + "audioOnly": "Audio only", + "featureToggleDisabled": "Toggling of __feature__ is disabled while in audio only mode", + "howToDisable": "Audio only mode is currently enabled. Click the audio only button in the toolbar to disable the feature." + }, "userMedia": { "react-nativeGrantPermissions": "Select Allow when your browser asks for permissions.", "chromeGrantPermissions": "Select Allow when your browser asks for permissions.", @@ -92,6 +97,7 @@ "rejoinKeyTitle": "Rejoin" }, "toolbar": { + "audioonly": "Enable / Disable audio only mode (saves bandwidth)", "mute": "Mute / Unmute", "videomute": "Start / Stop camera", "authenticate": "Authenticate", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 14442b94d..76ec2d15f 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -711,6 +711,14 @@ UI.setVideoMuted = function (id, muted) { } }; +/** + * Triggers an update of remote video and large video displays so they may pick + * up any state changes that have occurred elsewhere. + * + * @returns {void} + */ +UI.updateAllVideos = () => VideoLayout.updateAllVideos(); + /** * Adds a listener that would be notified on the given type of event. * diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index e594f8f89..a4b11d28a 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -11,6 +11,7 @@ import AudioLevels from "../audio_levels/AudioLevels"; const ParticipantConnectionStatus = JitsiMeetJS.constants.participantConnectionStatus; +const DESKTOP_CONTAINER_TYPE = 'desktop'; /** * Manager for all Large containers. @@ -33,7 +34,7 @@ export default class LargeVideoManager { this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer); // use the same video container to handle desktop tracks - this.addContainer("desktop", this.videoContainer); + this.addContainer(DESKTOP_CONTAINER_TYPE, this.videoContainer); this.width = 0; this.height = 0; @@ -103,6 +104,8 @@ export default class LargeVideoManager { preUpdate.then(() => { const { id, stream, videoType, resolve } = this.newStreamData; + const isVideoFromCamera = videoType === VIDEO_CONTAINER_TYPE; + this.newStreamData = null; logger.info("hover in %s", id); @@ -120,9 +123,7 @@ export default class LargeVideoManager { // If the container is VIDEO_CONTAINER_TYPE, we need to check // its stream whether exist and is muted to set isVideoMuted // in rest of the cases it is false - let showAvatar - = (videoType === VIDEO_CONTAINER_TYPE) - && (!stream || stream.isMuted()); + let showAvatar = isVideoFromCamera && (!stream || stream.isMuted()); // If the user's connection is disrupted then the avatar will be // displayed in case we have no video image cached. That is if @@ -130,12 +131,20 @@ export default class LargeVideoManager { // the video was not rendered, before the connection has failed. const isConnectionActive = this._isConnectionActive(id); - if (videoType === VIDEO_CONTAINER_TYPE + if (isVideoFromCamera && !isConnectionActive && (isUserSwitch || !container.wasVideoRendered)) { showAvatar = true; } + // If audio only mode is enabled, always show the avatar for + // videos from another participant. + if (APP.conference.isAudioOnly() + && (isVideoFromCamera + || videoType === DESKTOP_CONTAINER_TYPE)) { + showAvatar = true; + } + let promise; // do not show stream if video is muted @@ -159,8 +168,12 @@ export default class LargeVideoManager { // Make sure no notification about remote failure is shown as // its UI conflicts with the one for local connection interrupted. - const isConnected = APP.conference.isConnectionInterrupted() - || isConnectionActive; + // For the purposes of UI indicators, audio only is considered as + // an "active" connection. + const isConnected + = APP.conference.isAudioOnly() + || APP.conference.isConnectionInterrupted() + || isConnectionActive; // when isHavingConnectivityIssues, state can be inactive, // interrupted or restoring. We show different message for diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index dfdbc1255..faaaccb5e 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -556,6 +556,7 @@ RemoteVideo.prototype.isVideoPlayable = function () { * @inheritDoc */ RemoteVideo.prototype.updateView = function () { + $(this.container).toggleClass('audio-only', APP.conference.isAudioOnly()); this.updateConnectionStatusIndicator(); diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index aa3d12423..81d313cbb 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -459,7 +459,9 @@ SmallVideo.prototype.selectDisplayMode = function() { // Display name is always and only displayed when user is on the stage if (this.isCurrentlyOnLargeVideo()) { return DISPLAY_BLACKNESS_WITH_NAME; - } else if (this.isVideoPlayable() && this.selectVideoElement().length) { + } else if (this.isVideoPlayable() + && this.selectVideoElement().length + && !APP.conference.isAudioOnly()) { // check hovering and change state to video with name return this._isHovered() ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 96a61093d..920030bbb 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -956,6 +956,24 @@ var VideoLayout = { return largeVideo && largeVideo.id === id; }, + /** + * Triggers an update of remote video and large video displays so they may + * pick up any state changes that have occurred elsewhere. + * + * @returns {void} + */ + updateAllVideos() { + const displayedUserId = this.getLargeVideoID(); + + if (displayedUserId) { + this.updateLargeVideo(displayedUserId, true); + } + + Object.keys(remoteVideos).forEach(video => { + remoteVideos[video].updateView(); + }); + }, + updateLargeVideo (id, forceUpdate) { if (!largeVideo) { return; diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js index 170bb0a76..2c42f2b16 100644 --- a/react/features/base/conference/middleware.js +++ b/react/features/base/conference/middleware.js @@ -1,4 +1,6 @@ /* global APP */ +import UIEvents from '../../../../service/UI/UIEvents'; + import { CONNECTION_ESTABLISHED } from '../connection'; import { getLocalParticipant, @@ -149,6 +151,12 @@ function _setAudioOnly(store, next, action) { // Mute local video store.dispatch(_setAudioOnlyVideoMuted(audioOnly)); + if (typeof APP !== 'undefined') { + // TODO This should be a temporary solution that lasts only until + // video tracks and all ui is moved into react/redux on the web. + APP.UI.emitEvent(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly); + } + return result; } diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js index bba582c80..d685af169 100644 --- a/react/features/conference/components/Conference.web.js +++ b/react/features/conference/components/Conference.web.js @@ -7,6 +7,7 @@ import { connect, disconnect } from '../../base/connection'; import { DialogContainer } from '../../base/dialog'; import { Watermarks } from '../../base/react'; import { OverlayContainer } from '../../overlay'; +import { StatusLabel } from '../../status-label'; import { Toolbox } from '../../toolbox'; import { HideNotificationBarStyle } from '../../unsupported-browser'; @@ -95,6 +96,7 @@ class Conference extends Component { HD + + + + ); + } + + /** + * Sets the instance variable for the component's root element so it can be + * accessed directly. + * + * @param {HTMLDivElement} element - The topmost DOM element of the + * component's DOM/HTML hierarchy. + * @private + * @returns {void} + */ + _setRootElement(element) { + this._rootElement = element; + } + + /** + * Sets the tooltip on the component's root element. + * + * @private + * @returns {void} + */ + _setTooltip() { + UIUtil.setTooltip( + this._rootElement, + 'audioOnly.howToDisable', + 'left' + ); + } +} + +export default translate(AudioOnlyLabel); diff --git a/react/features/status-label/components/StatusLabel.js b/react/features/status-label/components/StatusLabel.js new file mode 100644 index 000000000..79fbd877e --- /dev/null +++ b/react/features/status-label/components/StatusLabel.js @@ -0,0 +1,58 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import AudioOnlyLabel from './AudioOnlyLabel'; + +/** + * Component responsible for displaying a label that indicates some state of the + * current conference. The AudioOnlyLabel component will be displayed when the + * conference is in audio only mode. + */ +export class StatusLabel extends Component { + /** + * StatusLabel component's property types. + * + * @static + */ + static propTypes = { + /** + * The redux store representation of the current conference. + */ + _conference: React.PropTypes.object + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement|null} + */ + render() { + if (!this.props._conference.audioOnly) { + return null; + } + + return ( +
+ +
+ ); + } +} + +/** + * Maps (parts of) the Redux state to the associated StatusLabel's props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _conference: Object, + * }} + */ +function _mapStateToProps(state) { + return { + _conference: state['features/base/conference'] + }; +} + +export default connect(_mapStateToProps)(StatusLabel); diff --git a/react/features/status-label/components/index.js b/react/features/status-label/components/index.js new file mode 100644 index 000000000..03dae51f7 --- /dev/null +++ b/react/features/status-label/components/index.js @@ -0,0 +1 @@ +export { default as StatusLabel } from './StatusLabel'; diff --git a/react/features/status-label/index.js b/react/features/status-label/index.js new file mode 100644 index 000000000..07635cbbc --- /dev/null +++ b/react/features/status-label/index.js @@ -0,0 +1 @@ +export * from './components'; diff --git a/react/features/toolbox/components/AudioOnlyButton.js b/react/features/toolbox/components/AudioOnlyButton.js new file mode 100644 index 000000000..f61bf8886 --- /dev/null +++ b/react/features/toolbox/components/AudioOnlyButton.js @@ -0,0 +1,103 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { toggleAudioOnly } from '../../base/conference'; + +import ToolbarButton from './ToolbarButton'; + +/** + * React {@code Component} for toggling audio only mode. + * + * @extends Component + */ +class AudioOnlyButton extends Component { + /** + * {@code AudioOnlyButton}'s property types. + * + * @static + */ + static propTypes = { + /** + * Whether or not audio only mode is enabled. + */ + _audioOnly: React.PropTypes.bool, + + /** + * Invoked to toggle audio only mode. + */ + dispatch: React.PropTypes.func, + + /** + * From which side the button tooltip should appear. + */ + tooltipPosition: React.PropTypes.string + } + + /** + * Initializes a new {@code AudioOnlyButton} instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props) { + super(props); + + // Bind event handlers so they are only bound once for every instance. + this._onClick = this._onClick.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const buttonConfiguration = { + buttonName: 'audioonly', + classNames: [ 'button', 'icon-visibility' ], + enabled: true, + id: 'toolbar_button_audioonly', + tooltipKey: 'toolbar.audioonly' + }; + + if (this.props._audioOnly) { + buttonConfiguration.classNames.push('toggled button-active'); + } + + return ( + + ); + } + + /** + * Dispatches an action to toggle audio only mode. + * + * @private + * @returns {void} + */ + _onClick() { + this.props.dispatch(toggleAudioOnly()); + } +} + +/** + * Maps (parts of) the Redux state to the associated {@code AudioOnlyButton}'s + * props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _audioOnly: boolean + * }} + */ +function _mapStateToProps(state) { + return { + _audioOnly: state['features/base/conference'].audioOnly + }; +} + +export default connect(_mapStateToProps)(AudioOnlyButton); diff --git a/react/features/toolbox/components/Toolbar.web.js b/react/features/toolbox/components/Toolbar.web.js index 38f3cd777..72e7df9bc 100644 --- a/react/features/toolbox/components/Toolbar.web.js +++ b/react/features/toolbox/components/Toolbar.web.js @@ -121,6 +121,17 @@ class Toolbar extends Component { _renderToolbarButton(acc: Array<*>, keyValuePair: Array<*>, index: number): Array> { const [ key, button ] = keyValuePair; + + if (button.component) { + acc.push( + + ); + + return acc; + } + const { splitterIndex, tooltipPosition } = this.props; if (splitterIndex && index === splitterIndex) { diff --git a/react/features/toolbox/components/ToolbarButton.web.js b/react/features/toolbox/components/ToolbarButton.web.js index a44d9ce34..d05c9d11d 100644 --- a/react/features/toolbox/components/ToolbarButton.web.js +++ b/react/features/toolbox/components/ToolbarButton.web.js @@ -185,7 +185,7 @@ class ToolbarButton extends AbstractToolbarButton { gravity = popup.dataAttrPosition; } - const title = this.props.t(popup.dataAttr); + const title = this.props.t(popup.dataAttr, popup.dataInterpolate); return (