diff --git a/conference.js b/conference.js index c768c4386..16516183f 100644 --- a/conference.js +++ b/conference.js @@ -43,7 +43,8 @@ import { lockStateChanged, onStartMutedPolicyChanged, p2pStatusChanged, - sendLocalParticipant + sendLocalParticipant, + setDesktopSharingEnabled } from './react/features/base/conference'; import { updateDeviceList } from './react/features/base/devices'; import { @@ -104,6 +105,7 @@ import { mediaPermissionPromptVisibilityChanged, suspendDetected } from './react/features/overlay'; +import { setSharedVideoStatus } from './react/features/shared-video'; import { isButtonEnabled, showDesktopSharingButton @@ -505,16 +507,6 @@ export default { */ desktopSharingDisabledTooltip: null, - /* - * Whether the local "raisedHand" flag is on. - */ - isHandRaised: false, - - /* - * Whether the local participant is the dominant speaker in the conference. - */ - isDominantSpeaker: false, - /** * The local audio track (if any). * FIXME tracks from redux store should be the single source of truth @@ -773,6 +765,8 @@ export default { JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED, this.isDesktopSharingEnabled); + APP.store.dispatch( + setDesktopSharingEnabled(this.isDesktopSharingEnabled)); APP.store.dispatch(showDesktopSharingButton()); this._createRoom(tracks); @@ -1896,19 +1890,6 @@ export default { }); room.on(JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, id => { APP.store.dispatch(dominantSpeakerChanged(id)); - - if (this.isLocalId(id)) { - this.isDominantSpeaker = true; - this.setRaisedHand(false); - } else { - this.isDominantSpeaker = false; - const participant = room.getParticipantById(id); - - if (participant) { - APP.UI.setRaisedHandStatus(participant, false); - } - } - APP.UI.markDominantSpeaker(id); }); if (!interfaceConfig.filmStripOnly) { @@ -2022,7 +2003,10 @@ export default { (participant, name, oldValue, newValue) => { switch (name) { case 'raisedHand': - APP.UI.setRaisedHandStatus(participant, newValue); + APP.store.dispatch(participantUpdated({ + id: participant.getId(), + raisedHand: newValue === 'true' + })); break; case 'remoteControlSessionStatus': APP.UI.setRemoteControlActiveStatus( @@ -2361,6 +2345,8 @@ export default { } }); } + + APP.store.dispatch(setSharedVideoStatus(state)); }); room.addCommandListener( this.commands.defaults.SHARED_VIDEO, @@ -2623,30 +2609,6 @@ export default { APP.API.notifyVideoAvailabilityChanged(available); }, - /** - * Toggles the local "raised hand" status. - */ - maybeToggleRaisedHand() { - this.setRaisedHand(!this.isHandRaised); - }, - - /** - * Sets the local "raised hand" status to a particular value. - */ - setRaisedHand(raisedHand) { - if (raisedHand !== this.isHandRaised) { - APP.UI.onLocalRaiseHandChanged(raisedHand); - - this.isHandRaised = raisedHand; - - // Advertise the updated status - room.setLocalParticipantProperty('raisedHand', raisedHand); - - // Update the view - APP.UI.setLocalRaisedHandStatus(raisedHand); - } - }, - /** * Disconnect from the conference and optionally request user feedback. * @param {boolean} [requestFeedback=false] if user feedback should be diff --git a/css/_filmstrip.scss b/css/_filmstrip.scss index 908e1946c..282fd84eb 100644 --- a/css/_filmstrip.scss +++ b/css/_filmstrip.scss @@ -5,6 +5,20 @@ justify-content: flex-start; } +.use-new-toolbox { + .filmstrip.reduce-height { + bottom: $newToolbarSize; + } + + .filmstrip { + transition: bottom .3s; + } + + .filmstrip__videos.hidden { + bottom: calc(-196px - #{$newToolbarSize}); + } +} + .filmstrip { position: absolute; bottom: 0; diff --git a/css/_font.scss b/css/_font.scss index fde7f6f77..15678354e 100644 --- a/css/_font.scss +++ b/css/_font.scss @@ -180,3 +180,6 @@ .icon-gsm-bars:before { content: "\e926"; } +.icon-open_in_new:before { + content: "\e89e"; +} diff --git a/css/_side_toolbar_container.scss b/css/_side_toolbar_container.scss index e9c27b419..3b0c161ce 100644 --- a/css/_side_toolbar_container.scss +++ b/css/_side_toolbar_container.scss @@ -1,6 +1,37 @@ /** * Toolbar side panel main container element. */ +.use-new-toolbox #sideToolbarContainer { + background-color: rgba(40, 52, 71, 0.5); + + /** + * Make the sidebar flush with the top of the toolbar. Take the size of + * the toolbar, plus its padding, and subtract from 100%. + */ + height: calc(100% - #{$newToolbarSize} - 10px); + left: 0; + + .side-toolbar-close { + background: gray; + border: 3px solid rgba(255, 255, 255, 0.1); + border-radius: 100%; + color: white; + cursor:pointer; + height: 10px; + line-height: 10px; + padding: 4px; + position: absolute; + right: 5px; + text-align: center; + top: 5px; + width: 10px; + z-index: 1; + } + + #chatconversation { + top: 15px; + } +} #sideToolbarContainer { background-color: $sideToolbarContainerBg; height: 100%; diff --git a/css/_toolbars.scss b/css/_toolbars.scss index 8dd34b4e9..bace72e91 100644 --- a/css/_toolbars.scss +++ b/css/_toolbars.scss @@ -261,6 +261,217 @@ } } +/** + * TODO: when the old filmstrip has been removed, remove the "new-" prefix. + */ +.new-toolbox { + background-color: rgba(40, 52, 71, 0.5); + bottom: calc((#{$newToolbarSize} * 2) * -1); + box-sizing: border-box; + display: flex; + justify-content: space-between; + padding: 5px 20px; + position: absolute; + transition: bottom .3s ease-in; + width: 100%; + z-index: $toolbarZ; + + &.visible { + bottom: 0; + } + + &.no-buttons { + display: none; + } + + .button-group-center, + .button-group-left, + .button-group-right { + display: flex; + width: 33%; + } + + .button-group-center { + justify-content: center; + } + + .button-group-right { + justify-content: flex-end; + } + + /** + * Overwrite font-awesome styling to match jitsi-icon styling. + */ + .fa { + font-size: 1.22em; + } + + i { + border-radius: 5px; + cursor: pointer; + display: block; + height: 100%; + line-height: inherit; + width: 100%; + } + + i:hover { + background-color: rgba(40, 52, 71, 0.7); + } + + i.toggled { + background: rgba(40, 52, 71, 1); + } + + i.toggled:hover { + background-color: rgba(40, 52, 71, 1); + } + + i.disabled { + cursor: initial + } + + i.disabled:hover { + background-color: initial; + } + + .icon-hangup { + color: $hangupColor; + } + + .overflow-menu { + font-size: 1.2em; + list-style-type: none; + /** + * Undo atlaskit padding by reducing margins. + */ + margin: -15px -24px; + padding: 0; + + .overflow-menu-item { + align-items: center; + cursor: pointer; + display: flex; + padding: 5px 10px; + + &:hover { + background: rgba(0255, 255, 255, 0.2); + } + + &.unclickable { + cursor: default; + } + &.unclickable:hover { + background: inherit; + } + } + + .overflow-menu-item-icon { + margin-right: 10px; + + i { + display: inline; + } + + i:hover { + background-color: initial; + } + + img { + max-width: 18px; + max-height: 18px; + } + } + + .profile-text { + max-width: 150px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + + .toolbox-button { + color: $toolbarButtonColor; + cursor: pointer; + display: inline-block; + font-size: $newToolbarFontSize; + line-height: $newToolbarSize; + margin: 0 10px; + text-align: center; + } + + .toolbar-button-with-badge { + position: relative; + + .badge-round { + bottom: 9px; + position: absolute; + right: 9px; + } + } + + .toolbox-button-wth-dialog { + display: inline-block; + } + + .toolbox-icon { + height: $newToolbarSize; + width: $newToolbarSize; + } +} + +.filmstrip-toolbox { + background-color: rgba(40, 52, 71, 0.5); + box-sizing: border-box; + display: flex; + flex-direction: column; + z-index: $toolbarZ; + + i { + cursor: pointer; + display: block; + font-size: $newToolbarFontSize; + height: 37px; + line-height: 37px; + width: 37px; + } + + i:hover { + background-color: rgba(40, 52, 71, 0.7); + } + + i.toggled { + background: rgba(40, 52, 71, 1); + } + + i.toggled:hover { + background-color: rgba(40, 52, 71, 1); + } + + .icon-hangup { + color: $hangupColor; + } + + .toolbox-button { + color: $toolbarButtonColor; + cursor: pointer; + text-align: center; + } + + border-radius: 3px; + + .toolbox-button:first-child i { + border-top-left-radius: 3px; + border-top-right-radius: 3px; + } + + .toolbox-button:last-child i { + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + } +} + .filmstrip-only { .toolbox, .toolbox-toolbars { diff --git a/css/_variables.scss b/css/_variables.scss index 72b988c8e..6464b0b42 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -36,6 +36,8 @@ $alwaysOnTopToolbarFontSize: 1em; $alwaysOnTopToolbarSize: 30px; $defaultToolbarSize: 50px; $defaultFilmStripOnlyToolbarSize: 37px; +$newToolbarSize: 50px; +$newToolbarFontSize: 1.9em; $secToolbarFontSize: 1.9em; $secToolbarLineHeight: 45px; $toolbarAvatarPadding: 10px; diff --git a/css/_vertical_filmstrip_overrides.scss b/css/_vertical_filmstrip_overrides.scss index cde25fcbf..18e7a2957 100644 --- a/css/_vertical_filmstrip_overrides.scss +++ b/css/_vertical_filmstrip_overrides.scss @@ -19,6 +19,20 @@ text-align: left; } + &.use-new-toolbox { + /** + * Adjust the height of the filmstrip as the toolbar is displayed. + */ + .filmstrip { + top: 0; + transition: height .3s ease-in; + + &.reduce-height { + height: calc(100% - #{$newToolbarSize}); + } + } + } + .filmstrip { align-items: flex-end; box-sizing: border-box; @@ -32,14 +46,7 @@ * any parent is also fixed. */ position: fixed; - - /** - * z-index adjusting is needed because the video state indicator has to - * display over the filmstrip when no videos are displayed but still be - * clickable but its inline dialogs must display over the video state - * indicator when videos are displayed. - */ - z-index: #{$tooltipsZ + 1}; + z-index: $filmstripVideosZ; /** * Hide videos by making them slight to the right. diff --git a/css/modals/video-quality/_video-quality.scss b/css/modals/video-quality/_video-quality.scss index f7e3c3a0e..80b4e8715 100644 --- a/css/modals/video-quality/_video-quality.scss +++ b/css/modals/video-quality/_video-quality.scss @@ -135,6 +135,12 @@ } } +.modal-dialog-form { + .video-quality-dialog-title { + display: none; + } +} + .video-state-indicator { background: $videoStateIndicatorBackground; cursor: default; @@ -162,11 +168,11 @@ } .centeredVideoLabel.moveToCorner { - z-index: $tooltipsZ; + z-index: $zindex3; } #videoResolutionLabel { - z-index: #{$tooltipsZ + 1}; + z-index: $zindex3 + 1; } .centeredVideoLabel { diff --git a/fonts/jitsi.eot b/fonts/jitsi.eot index b4328bb8d..0974a8822 100755 Binary files a/fonts/jitsi.eot and b/fonts/jitsi.eot differ diff --git a/fonts/jitsi.svg b/fonts/jitsi.svg index 4a9b9bf7f..a1ec8c803 100755 --- a/fonts/jitsi.svg +++ b/fonts/jitsi.svg @@ -23,6 +23,7 @@ + diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf index bd290129d..e922ca946 100755 Binary files a/fonts/jitsi.ttf and b/fonts/jitsi.ttf differ diff --git a/fonts/jitsi.woff b/fonts/jitsi.woff index 81d8033bb..f5b44e028 100755 Binary files a/fonts/jitsi.woff and b/fonts/jitsi.woff differ diff --git a/fonts/selection.json b/fonts/selection.json index 52f079d3f..7c30e352f 100755 --- a/fonts/selection.json +++ b/fonts/selection.json @@ -1,6 +1,33 @@ { "IcoMoonType": "selection", "icons": [ + { + "icon": { + "paths": [ + "M598 128h298v298h-86v-152l-418 418-60-60 418-418h-152v-86zM810 810v-298h86v298c0 46-40 86-86 86h-596c-48 0-86-40-86-86v-596c0-46 38-86 86-86h298v86h-298v596h596z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "open_in_new" + ], + "defaultCode": 59550, + "grid": 24 + }, + "attrs": [], + "properties": { + "ligatures": "launch, open_in_new", + "id": 761, + "order": 932, + "prevSize": 24, + "name": "open_in_new", + "code": 59550 + }, + "setIdx": 0, + "setId": 1, + "iconIdx": 0 + }, { "icon": { "paths": [ @@ -18,15 +45,15 @@ "attrs": [], "properties": { "ligatures": "history, restore", - "id": 385, - "order": 930, + "id": 762, + "order": 933, "prevSize": 24, "code": 59571, "name": "restore" }, "setIdx": 0, - "setId": 2, - "iconIdx": 385 + "setId": 1, + "iconIdx": 1 }, { "icon": { @@ -45,15 +72,15 @@ "attrs": [], "properties": { "ligatures": "chevron_right, navigate_next", - "id": 153, - "order": 927, + "id": 763, + "order": 934, "prevSize": 24, "code": 58377, "name": "navigate_next" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 0 + "iconIdx": 2 }, { "icon": { @@ -72,15 +99,15 @@ "attrs": [], "properties": { "ligatures": "menu", - "id": 489, - "order": 926, + "id": 764, + "order": 935, "prevSize": 24, "code": 58834, "name": "menu" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 1 + "iconIdx": 3 }, { "icon": { @@ -99,15 +126,15 @@ "attrs": [], "properties": { "ligatures": "arrow_back", - "id": 45, - "order": 924, + "id": 765, + "order": 936, "prevSize": 24, "code": 58820, "name": "arrow_back" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 2 + "iconIdx": 4 }, { "icon": { @@ -126,15 +153,15 @@ "attrs": [], "properties": { "ligatures": "chevron_left, navigate_before", - "id": 152, - "order": 923, + "id": 766, + "order": 937, "prevSize": 24, "code": 58376, "name": "navigate_before" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 3 + "iconIdx": 5 }, { "icon": { @@ -153,15 +180,15 @@ "attrs": [], "properties": { "ligatures": "public", - "id": 605, - "order": 920, + "id": 767, + "order": 938, "prevSize": 24, "code": 59403, "name": "public" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 4 + "iconIdx": 6 }, { "icon": { @@ -180,15 +207,15 @@ "attrs": [], "properties": { "ligatures": "event_note", - "id": 252, - "order": 919, + "id": 768, + "order": 939, "prevSize": 24, "code": 58902, "name": "event_note" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 5 + "iconIdx": 7 }, { "icon": { @@ -207,15 +234,15 @@ "attrs": [], "properties": { "ligatures": "timer", - "id": 760, - "order": 928, + "id": 769, + "order": 940, "prevSize": 24, "code": 58405, "name": "timer" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 6 + "iconIdx": 8 }, { "icon": { @@ -234,15 +261,15 @@ "attrs": [], "properties": { "ligatures": "bluetooth_audio, bluetooth_searching", - "id": 79, - "order": 911, + "id": 770, + "order": 941, "prevSize": 24, "code": 57770, "name": "bluetooth" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 7 + "iconIdx": 9 }, { "icon": { @@ -261,15 +288,15 @@ "attrs": [], "properties": { "ligatures": "headset", - "id": 376, - "order": 910, + "id": 771, + "order": 942, "prevSize": 24, "code": 58128, "name": "headset" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 8 + "iconIdx": 10 }, { "icon": { @@ -288,15 +315,15 @@ "attrs": [], "properties": { "ligatures": "phone_in_talk", - "id": 566, - "order": 912, + "id": 772, + "order": 943, "prevSize": 24, "code": 58909, "name": "phone-talk" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 9 + "iconIdx": 11 }, { "icon": { @@ -315,15 +342,15 @@ "attrs": [], "properties": { "ligatures": "more_vert", - "id": 0, - "order": 897, + "id": 773, + "order": 944, "prevSize": 24, "code": 58836, "name": "thumb-menu" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 10 + "iconIdx": 12 }, { "icon": { @@ -344,15 +371,15 @@ {} ], "properties": { - "order": 850, - "id": 1, + "order": 945, + "id": 774, "name": "ninja", "prevSize": 24, "code": 59657 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 11 + "iconIdx": 13 }, { "icon": { @@ -371,15 +398,15 @@ "attrs": [], "properties": { "ligatures": "call, local_phone, phone", - "id": 2, - "order": 851, + "id": 775, + "order": 946, "prevSize": 24, "code": 57549, "name": "phone" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 12 + "iconIdx": 14 }, { "icon": { @@ -398,15 +425,15 @@ "attrs": [], "properties": { "ligatures": "add", - "id": 3, - "order": 896, + "id": 776, + "order": 947, "prevSize": 24, "code": 57669, "name": "add" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 13 + "iconIdx": 15 }, { "icon": { @@ -427,15 +454,15 @@ {} ], "properties": { - "order": 901, - "id": 0, + "order": 948, + "id": 777, "name": "gsm-bars-black", "prevSize": 32, "code": 59686 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 14 + "iconIdx": 16 }, { "icon": { @@ -456,15 +483,15 @@ {} ], "properties": { - "order": 898, - "id": 0, + "order": 949, + "id": 778, "name": "info", "prevSize": 32, "code": 59682 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 15 + "iconIdx": 17 }, { "icon": { @@ -485,15 +512,15 @@ {} ], "properties": { - "order": 856, - "id": 0, + "order": 950, + "id": 779, "name": "mic-camera-combined", "prevSize": 32, "code": 59651 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 16 + "iconIdx": 18 }, { "icon": { @@ -514,15 +541,15 @@ {} ], "properties": { - "order": 857, - "id": 1, + "order": 951, + "id": 780, "name": "feedback", "prevSize": 32, "code": 59677 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 17 + "iconIdx": 19 }, { "icon": { @@ -543,15 +570,15 @@ {} ], "properties": { - "order": 858, - "id": 2, + "order": 952, + "id": 781, "name": "toggle-filmstrip", "prevSize": 32, "code": 59676 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 18 + "iconIdx": 20 }, { "icon": { @@ -568,16 +595,16 @@ }, "attrs": [], "properties": { - "id": 3, - "order": 859, + "id": 782, + "order": 953, "ligatures": "account_circle", "prevSize": 32, "code": 59649, "name": "avatar" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 19 + "iconIdx": 21 }, { "icon": { @@ -594,16 +621,16 @@ }, "attrs": [], "properties": { - "id": 4, - "order": 860, + "id": 783, + "order": 954, "ligatures": "call_end", "prevSize": 32, "code": 59653, "name": "hangup" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 20 + "iconIdx": 22 }, { "icon": { @@ -620,16 +647,16 @@ }, "attrs": [], "properties": { - "id": 5, - "order": 861, + "id": 784, + "order": 955, "ligatures": "chat_bubble_outline", "prevSize": 32, "code": 59654, "name": "chat" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 21 + "iconIdx": 23 }, { "icon": { @@ -646,16 +673,16 @@ }, "attrs": [], "properties": { - "id": 6, - "order": 862, + "id": 785, + "order": 956, "ligatures": "cloud_download", "prevSize": 32, "code": 59650, "name": "download" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 22 + "iconIdx": 24 }, { "icon": { @@ -672,16 +699,16 @@ }, "attrs": [], "properties": { - "id": 7, - "order": 863, + "id": 786, + "order": 957, "ligatures": "create, edit, mode_edit", "prevSize": 32, "code": 59655, "name": "edit" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 23 + "iconIdx": 25 }, { "icon": { @@ -698,16 +725,16 @@ }, "attrs": [], "properties": { - "id": 8, - "order": 864, + "id": 787, + "order": 958, "ligatures": "description", "prevSize": 32, "code": 59656, "name": "share-doc" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 24 + "iconIdx": 26 }, { "icon": { @@ -724,16 +751,16 @@ }, "attrs": [], "properties": { - "id": 9, - "order": 865, + "id": 788, + "order": 959, "ligatures": "eject", "prevSize": 32, "code": 59652, "name": "kick" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 25 + "iconIdx": 27 }, { "icon": { @@ -750,16 +777,16 @@ }, "attrs": [], "properties": { - "id": 10, - "order": 922, + "id": 789, + "order": 960, "ligatures": "expand_less", "prevSize": 32, "code": 59679, "name": "menu-up" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 26 + "iconIdx": 28 }, { "icon": { @@ -776,16 +803,16 @@ }, "attrs": [], "properties": { - "id": 11, - "order": 867, + "id": 790, + "order": 961, "ligatures": "expand_more", "prevSize": 32, "code": 59680, "name": "menu-down" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 27 + "iconIdx": 29 }, { "icon": { @@ -802,16 +829,16 @@ }, "attrs": [], "properties": { - "id": 12, - "order": 868, + "id": 791, + "order": 962, "ligatures": "fullscreen", "prevSize": 32, "code": 59659, "name": "full-screen" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 28 + "iconIdx": 30 }, { "icon": { @@ -828,16 +855,16 @@ }, "attrs": [], "properties": { - "id": 13, - "order": 869, + "id": 792, + "order": 963, "ligatures": "fullscreen_exit", "prevSize": 32, "code": 59660, "name": "exit-full-screen" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 29 + "iconIdx": 31 }, { "icon": { @@ -854,16 +881,16 @@ }, "attrs": [], "properties": { - "id": 14, - "order": 870, + "id": 793, + "order": 964, "ligatures": "grade, star", "prevSize": 32, "code": 59658, "name": "star-full" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 30 + "iconIdx": 32 }, { "icon": { @@ -880,16 +907,16 @@ }, "attrs": [], "properties": { - "id": 15, - "order": 871, + "id": 794, + "order": 965, "ligatures": "lock_open", "prevSize": 32, "code": 59661, "name": "security" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 31 + "iconIdx": 33 }, { "icon": { @@ -906,16 +933,16 @@ }, "attrs": [], "properties": { - "id": 16, - "order": 872, + "id": 795, + "order": 966, "ligatures": "lock_outline", "prevSize": 32, "code": 59662, "name": "security-locked" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 32 + "iconIdx": 34 }, { "icon": { @@ -932,16 +959,16 @@ }, "attrs": [], "properties": { - "id": 17, - "order": 873, + "id": 796, + "order": 967, "ligatures": "loop, sync", "prevSize": 32, "code": 59663, "name": "reload" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 33 + "iconIdx": 35 }, { "icon": { @@ -958,16 +985,16 @@ }, "attrs": [], "properties": { - "id": 18, - "order": 874, + "id": 797, + "order": 968, "ligatures": "mic", "prevSize": 32, "code": 59664, "name": "microphone" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 34 + "iconIdx": 36 }, { "icon": { @@ -984,16 +1011,16 @@ }, "attrs": [], "properties": { - "id": 19, - "order": 875, + "id": 798, + "order": 969, "ligatures": "mic_none", "prevSize": 32, "code": 59665, "name": "mic-empty" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 35 + "iconIdx": 37 }, { "icon": { @@ -1010,16 +1037,16 @@ }, "attrs": [], "properties": { - "id": 20, - "order": 876, + "id": 799, + "order": 970, "ligatures": "mic_off", "prevSize": 32, "code": 59666, "name": "mic-disabled" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 36 + "iconIdx": 38 }, { "icon": { @@ -1036,16 +1063,16 @@ }, "attrs": [], "properties": { - "id": 21, - "order": 899, + "id": 800, + "order": 971, "ligatures": "pan_tool", "prevSize": 32, "code": 59678, "name": "raised-hand" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 37 + "iconIdx": 39 }, { "icon": { @@ -1062,16 +1089,16 @@ }, "attrs": [], "properties": { - "id": 22, - "order": 878, + "id": 801, + "order": 972, "ligatures": "people_outline", "prevSize": 32, "code": 59675, "name": "contactList" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 38 + "iconIdx": 40 }, { "icon": { @@ -1088,16 +1115,16 @@ }, "attrs": [], "properties": { - "id": 23, - "order": 879, + "id": 802, + "order": 973, "ligatures": "person_add", "prevSize": 32, "code": 59667, "name": "link" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 39 + "iconIdx": 41 }, { "icon": { @@ -1114,16 +1141,16 @@ }, "attrs": [], "properties": { - "id": 24, - "order": 880, + "id": 803, + "order": 974, "ligatures": "play_circle_outline", "prevSize": 32, "code": 59668, "name": "shared-video" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 40 + "iconIdx": 42 }, { "icon": { @@ -1140,16 +1167,16 @@ }, "attrs": [], "properties": { - "id": 25, - "order": 881, + "id": 804, + "order": 975, "ligatures": "settings", "prevSize": 32, "code": 59669, "name": "settings" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 41 + "iconIdx": 43 }, { "icon": { @@ -1166,16 +1193,16 @@ }, "attrs": [], "properties": { - "id": 26, - "order": 882, + "id": 805, + "order": 976, "ligatures": "star_border", "prevSize": 32, "code": 59670, "name": "star" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 42 + "iconIdx": 44 }, { "icon": { @@ -1192,16 +1219,16 @@ }, "attrs": [], "properties": { - "id": 27, - "order": 883, + "id": 806, + "order": 977, "ligatures": "switch_camera", "prevSize": 32, "code": 59681, "name": "switch-camera" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 43 + "iconIdx": 45 }, { "icon": { @@ -1218,16 +1245,16 @@ }, "attrs": [], "properties": { - "id": 28, - "order": 884, + "id": 807, + "order": 978, "ligatures": "tv", "prevSize": 32, "code": 59671, "name": "share-desktop" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 44 + "iconIdx": 46 }, { "icon": { @@ -1244,16 +1271,16 @@ }, "attrs": [], "properties": { - "id": 29, - "order": 885, + "id": 808, + "order": 979, "ligatures": "videocam", "prevSize": 32, "code": 59672, "name": "camera" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 45 + "iconIdx": 47 }, { "icon": { @@ -1270,16 +1297,16 @@ }, "attrs": [], "properties": { - "id": 30, - "order": 886, + "id": 809, + "order": 980, "ligatures": "videocam_off", "prevSize": 32, "code": 59673, "name": "camera-disabled" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 46 + "iconIdx": 48 }, { "icon": { @@ -1296,16 +1323,16 @@ }, "attrs": [], "properties": { - "id": 31, - "order": 887, + "id": 810, + "order": 981, "ligatures": "volume_up", "prevSize": 32, "code": 59674, "name": "volume" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 47 + "iconIdx": 49 }, { "icon": { @@ -1325,16 +1352,16 @@ }, "attrs": [], "properties": { - "order": 890, - "id": 34, + "order": 982, + "id": 811, "prevSize": 32, "code": 58899, "name": "recDisable", "ligatures": "" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 48 + "iconIdx": 50 }, { "icon": { @@ -1355,16 +1382,16 @@ }, "attrs": [], "properties": { - "order": 891, - "id": 35, + "order": 983, + "id": 812, "prevSize": 32, "code": 58900, "name": "recEnable", "ligatures": "" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 49 + "iconIdx": 51 }, { "icon": { @@ -1385,16 +1412,16 @@ }, "attrs": [], "properties": { - "order": 892, - "id": 36, + "order": 984, + "id": 813, "prevSize": 32, "code": 58883, "name": "presentation", "ligatures": "" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 50 + "iconIdx": 52 }, { "icon": { @@ -1411,16 +1438,16 @@ }, "attrs": [], "properties": { - "order": 893, + "order": 985, "ligatures": "dialpad", - "id": 37, + "id": 814, "prevSize": 32, "code": 59685, "name": "dialpad" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 51 + "iconIdx": 53 }, { "icon": { @@ -1437,16 +1464,16 @@ }, "attrs": [], "properties": { - "order": 894, + "order": 986, "ligatures": "remove_red_eye, visibility", - "id": 38, + "id": 815, "prevSize": 32, "code": 59683, "name": "visibility" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 52 + "iconIdx": 54 }, { "icon": { @@ -1463,16 +1490,16 @@ }, "attrs": [], "properties": { - "order": 895, + "order": 987, "ligatures": "visibility_off", - "id": 39, + "id": 816, "prevSize": 32, "code": 59684, "name": "visibility-off" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 53 + "iconIdx": 55 } ], "height": 1024, diff --git a/interface_config.js b/interface_config.js index 8ed4d4eaa..91dac2f76 100644 --- a/interface_config.js +++ b/interface_config.js @@ -44,7 +44,10 @@ var interfaceConfig = { 'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup', // extended toolbar - 'profile', 'contacts', 'info', 'chat', 'recording', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip' ], + 'profile', 'contacts', 'info', 'chat', 'recording', 'etherpad', + 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip', + 'invite', 'feedback', 'stats', 'shortcuts' + ], /** * Main Toolbar Buttons @@ -150,7 +153,18 @@ var interfaceConfig = { * * @type {boolean} */ - VIDEO_QUALITY_LABEL_DISABLED: false + VIDEO_QUALITY_LABEL_DISABLED: false, + + /** + * This is a temporary feature flag used to gate access to the toolbox so it + * can be developed through smaller changesets. This feature flag will be + * removed at some point, as well as the old toolbox. This new toolbox will + * be horizontal and support for horizontal filmstrip will be removed, + * except in the case of interfaceConfig.filmStripOnly being true. + * + * @type {boolean} + */ + _USE_NEW_TOOLBOX: false /** * Specify custom URL for downloading android mobile app. diff --git a/lang/main.json b/lang/main.json index 6b9319624..3f9ea79a2 100644 --- a/lang/main.json +++ b/lang/main.json @@ -73,14 +73,23 @@ "toolbar": { "addPeople": "Add people to your call", "audioonly": "Enable / Disable audio only mode (saves bandwidth)", + "callQuality": "Manage call quality", + "enterFullScreen": "View full screen", + "exitFullScreen": "Exit full screen", + "feedback": "Leave feedback", + "moreActions": "More actions", "mute": "Mute / Unmute", "videomute": "Start / Stop camera", "authenticate": "Authenticate", "lock": "Lock / Unlock room", "chat": "Open / Close chat", "etherpad": "Open / Close shared document", + "documentOpen": "Open shared document", + "documentClose": "Close shared document", "sharedvideo": "Share a YouTube video", - "sharescreen": "Start / Stop screen sharing", + "sharescreen": "Screen share", + "sharescreenDisabled": "Screen share disabled", + "stopSharedVideo": "Stop YouTube video", "fullscreen": "View / Exit full screen", "sip": "Call SIP number", "Settings": "Settings", @@ -96,7 +105,9 @@ "micDisabled": "Microphone is not available", "filmstrip": "Show / Hide videos", "profile": "Edit your profile", - "raiseHand": "Raise / Lower your hand" + "raiseHand": "Raise / Lower your hand", + "shortcuts": "View shortcuts", + "speakerStats": "Speaker stats" }, "unsupportedBrowser": { "appNotInstalled": "Join this meeting with __app__ on your phone.", @@ -285,6 +296,7 @@ "liveStreaming": "Live Streaming", "streamKey": "Live stream key", "startLiveStreaming": "Go live now", + "startRecording": "Start recording", "stopStreamingWarning": "Are you sure you would like to stop the live streaming?", "stopRecordingWarning": "Are you sure you would like to stop the recording?", "stopLiveStreaming": "Stop live streaming", @@ -473,6 +485,7 @@ "loadingPeople": "Searching for people to invite", "noResults": "No matching search results", "noValidNumbers": "Please enter a phone number", + "notAvailable": "You can't invite people.", "searchNumbers": "Enter a phone number to invite", "searchPeople": "Enter a name to invite", "searchPeopleAndNumbers": "Enter a name or phone number to invite", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index ebd178d46..767cd282a 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -30,6 +30,7 @@ import { } from '../../react/features/base/participants'; import { destroyLocalTracks } from '../../react/features/base/tracks'; import { openDisplayNamePrompt } from '../../react/features/display-name'; +import { setEtherpadHasInitialzied } from '../../react/features/etherpad'; import { setNotificationsEnabled, showWarningNotification @@ -100,9 +101,6 @@ const UIListeners = new Map([ ], [ UIEvents.SHARED_VIDEO_CLICKED, () => sharedVideoManager && sharedVideoManager.toggleSharedVideo() - ], [ - UIEvents.TOGGLE_FULLSCREEN, - () => UI.toggleFullScreen() ], [ UIEvents.TOGGLE_CHAT, () => UI.toggleChat() @@ -135,14 +133,6 @@ const UIListeners = new Map([ ] ]); -/** - * Toggles the application in and out of full screen mode - * (a.k.a. presentation mode in Chrome). - */ -UI.toggleFullScreen = function() { - UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen(); -}; - /** * Indicates if we're currently in full screen mode. * @@ -255,12 +245,20 @@ UI.showLocalConnectionInterrupted = function(isInterrupted) { /** * Sets the "raised hand" status for a participant. + * + * @param {string} id - The id of the participant whose raised hand UI should + * be updated. + * @param {string} name - The name of the participant with the raised hand + * update. + * @param {boolean} raisedHandStatus - Whether the participant's hand is raised + * or not. + * @returns {void} */ -UI.setRaisedHandStatus = (participant, raisedHandStatus) => { - VideoLayout.setRaisedHandStatus(participant.getId(), raisedHandStatus); +UI.setRaisedHandStatus = (id, name, raisedHandStatus) => { + VideoLayout.setRaisedHandStatus(id, raisedHandStatus); if (raisedHandStatus) { messageHandler.participantNotification( - participant.getDisplayName(), + name, 'notify.somebody', 'connected', 'notify.raisedHand'); @@ -374,6 +372,14 @@ UI.start = function() { $('body').addClass('vertical-filmstrip'); } + + // TODO: remove this class once the old toolbar has been removed. This class + // is set so that any CSS changes needed to adjust elements outside of the + // new toolbar can be scoped to just the app with the new toolbar enabled. + if (interfaceConfig._USE_NEW_TOOLBOX && !interfaceConfig.filmStripOnly) { + $('body').addClass('use-new-toolbox'); + } + document.title = interfaceConfig.APP_NAME; }; @@ -404,12 +410,7 @@ UI.bindEvents = () => { // Resize and reposition videos in full screen mode. $(document).on( 'webkitfullscreenchange mozfullscreenchange fullscreenchange', - () => { - eventEmitter.emit( - UIEvents.FULLSCREEN_TOGGLED, - UIUtil.isFullScreen()); - onResize(); - }); + onResize); $(window).resize(onResize); }; @@ -474,6 +475,7 @@ UI.initEtherpad = name => { etherpadManager = new EtherpadManager(config.etherpad_base, name, eventEmitter); + APP.store.dispatch(setEtherpadHasInitialzied()); APP.store.dispatch(showEtherpadButton()); }; diff --git a/modules/UI/etherpad/Etherpad.js b/modules/UI/etherpad/Etherpad.js index 77a77f596..011422432 100644 --- a/modules/UI/etherpad/Etherpad.js +++ b/modules/UI/etherpad/Etherpad.js @@ -1,4 +1,7 @@ -/* global $, interfaceConfig */ +/* global $, APP, interfaceConfig */ + +import { setDocumentEditingState } from '../../../react/features/etherpad'; +import { getToolboxHeight } from '../../../react/features/toolbox'; import VideoLayout from '../videolayout/VideoLayout'; import LargeContainer from '../videolayout/LargeContainer'; @@ -126,7 +129,8 @@ class Etherpad extends LargeContainer { let height, width; if (interfaceConfig.VERTICAL_FILMSTRIP) { - height = containerHeight; + height = interfaceConfig._USE_NEW_TOOLBOX + ? containerHeight - getToolboxHeight() : containerHeight; width = containerWidth - Filmstrip.getFilmstripWidth(); } else { height = containerHeight - Filmstrip.getFilmstripHeight(); @@ -242,5 +246,7 @@ export default class EtherpadManager { this.eventEmitter .emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible); + + APP.store.dispatch(setDocumentEditingState(!isVisible)); } } diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index fcbdca41e..b38a5ad3b 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -35,6 +35,7 @@ import { StartLiveStreamDialog, StopLiveStreamDialog, hideRecordingLabel, + setRecordingType, updateRecordingState } from '../../../react/features/recording'; @@ -202,6 +203,8 @@ const Recording = { this.eventEmitter = eventEmitter; this.recordingType = recordingType; + APP.store.dispatch(setRecordingType(recordingType)); + this.updateRecordingState(APP.conference.getRecordingState()); if (recordingType === 'jibri') { @@ -219,6 +222,9 @@ const Recording = { '#toolbar_button_record', ev => this._onToolbarButtonClick(ev)); + this.eventEmitter.on(UIEvents.TOGGLE_RECORDING, + () => this._onToolbarButtonClick()); + // If I am a recorder then I publish my recorder custom role to notify // everyone. if (config.iAmRecorder) { @@ -287,6 +293,7 @@ const Recording = { this.currentState = recordingState; let labelDisplayConfiguration; + let isRecording = false; switch (recordingState) { case JitsiRecordingStatus.ON: @@ -298,6 +305,7 @@ const Recording = { }; this._setToolbarButtonToggled(true); + isRecording = true; break; } @@ -362,6 +370,7 @@ const Recording = { } APP.store.dispatch(updateRecordingState({ + isRecording, labelDisplayConfiguration, recordingState })); diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index a28c9bd70..c9ad43b4c 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -18,7 +18,11 @@ import { participantJoined, participantLeft } from '../../../react/features/base/participants'; -import { dockToolbox, showToolbox } from '../../../react/features/toolbox'; +import { + dockToolbox, + getToolboxHeight, + showToolbox +} from '../../../react/features/toolbox'; import SharedVideoThumb from './SharedVideoThumb'; @@ -695,7 +699,8 @@ class SharedVideoContainer extends LargeContainer { let height, width; if (interfaceConfig.VERTICAL_FILMSTRIP) { - height = containerHeight; + height = interfaceConfig._USE_NEW_TOOLBOX + ? containerHeight - getToolboxHeight() : containerHeight; width = containerWidth - Filmstrip.getFilmstripWidth(); } else { height = containerHeight - Filmstrip.getFilmstripHeight(); diff --git a/modules/UI/side_pannels/SideContainerToggler.js b/modules/UI/side_pannels/SideContainerToggler.js index 37900c67c..5e214def4 100644 --- a/modules/UI/side_pannels/SideContainerToggler.js +++ b/modules/UI/side_pannels/SideContainerToggler.js @@ -1,5 +1,6 @@ -/* global $ */ +/* global $, APP */ import UIEvents from '../../../service/UI/UIEvents'; +import { setVisiblePanel } from '../../../react/features/side-panel'; /** * Handles open and close of the extended toolbar side panel @@ -57,6 +58,7 @@ const SideContainerToggler = { if (isSelectorVisible) { this.hide(); + APP.store.dispatch(setVisiblePanel(null)); } else { if (this.isVisible()) { $('#sideToolbarContainer').children() @@ -74,6 +76,7 @@ const SideContainerToggler = { } this.showInnerContainer(elementSelector); + APP.store.dispatch(setVisiblePanel(elementId)); } }, diff --git a/modules/UI/side_pannels/chat/Chat.js b/modules/UI/side_pannels/chat/Chat.js index e335771ee..c9d917d4b 100644 --- a/modules/UI/side_pannels/chat/Chat.js +++ b/modules/UI/side_pannels/chat/Chat.js @@ -1,4 +1,4 @@ -/* global APP, $ */ +/* global APP, $, interfaceConfig */ import { processReplacements, linkify } from './Replacement'; import CommandsProcessor from './Commands'; @@ -9,7 +9,12 @@ import UIEvents from '../../../../service/UI/UIEvents'; import { smileys } from './smileys'; -import { dockToolbox, setSubject } from '../../../../react/features/toolbox'; +import { addMessage, markAllRead } from '../../../../react/features/chat'; +import { + dockToolbox, + getToolboxHeight, + setSubject +} from '../../../../react/features/toolbox'; let unreadMessages = 0; const sidePanelsContainerId = 'sideToolbarContainer'; @@ -163,6 +168,8 @@ function addSmileys() { * Resizes the chat conversation. */ function resizeChatConversation() { + // FIXME: this function can all be done with CSS. If Chat is ever rewritten, + // do not copy over this logic. const msgareaHeight = $('#usermsg').outerHeight(); const chatspace = $(`#${CHAT_CONTAINER_ID}`); const width = chatspace.width(); @@ -173,7 +180,16 @@ function resizeChatConversation() { $('#smileys').css('bottom', (msgareaHeight - 26) / 2); $('#smileysContainer').css('bottom', msgareaHeight); chat.width(width - 10); - chat.height(window.innerHeight - 15 - msgareaHeight); + + if (interfaceConfig._USE_NEW_TOOLBOX) { + const maybeAMagicNumberForPaddingAndMargin = 100; + const offset = maybeAMagicNumberForPaddingAndMargin + + msgareaHeight + getToolboxHeight(); + + chat.height(window.innerHeight - offset); + } else { + chat.height(window.innerHeight - 15 - msgareaHeight); + } } /** @@ -249,6 +265,7 @@ const Chat = { } unreadMessages = 0; + APP.store.dispatch(markAllRead()); updateVisualNotification(); // Undock the toolbar when the chat is shown and if we're in a @@ -274,9 +291,10 @@ const Chat = { */ // eslint-disable-next-line max-params updateChatConversation(id, displayName, message, stamp) { + const isFromLocalParticipant = APP.conference.isLocalId(id); let divClassName = ''; - if (APP.conference.isLocalId(id)) { + if (isFromLocalParticipant) { divClassName = 'localuser'; } else { divClassName = 'remoteuser'; @@ -294,6 +312,7 @@ const Chat = { .replace(/>/g, '>') .replace(/\n/g, '
'); const escDisplayName = UIUtil.escapeHtml(displayName); + const timestamp = getCurrentTime(stamp); // eslint-disable-next-line no-param-reassign message = processReplacements(escMessage); @@ -302,13 +321,18 @@ const Chat = { = `${'
' + '' + '
${escDisplayName - }
${getCurrentTime(stamp) + }
${timestamp }
${message}
` + '
'; $('#chatconversation').append(messageContainer); $('#chatconversation').animate( { scrollTop: $('#chatconversation')[0].scrollHeight }, 1000); + + const markAsRead = Chat.isVisible() || isFromLocalParticipant; + + APP.store.dispatch(addMessage( + escDisplayName, message, timestamp, markAsRead)); }, /** diff --git a/modules/UI/util/UIUtil.js b/modules/UI/util/UIUtil.js index d6cf7412f..be71e5e21 100644 --- a/modules/UI/util/UIUtil.js +++ b/modules/UI/util/UIUtil.js @@ -227,43 +227,10 @@ const UIUtil = { * mode, {false} otherwise */ isFullScreen() { - return document.fullscreenElement + return Boolean(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement - || document.msFullscreenElement; - }, - - /** - * Exits full screen mode. - * @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API - */ - exitFullScreen() { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } - }, - - /** - * Enter full screen mode. - * @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API - */ - enterFullScreen() { - if (document.documentElement.requestFullscreen) { - document.documentElement.requestFullscreen(); - } else if (document.documentElement.msRequestFullscreen) { - document.documentElement.msRequestFullscreen(); - } else if (document.documentElement.mozRequestFullScreen) { - document.documentElement.mozRequestFullScreen(); - } else if (document.documentElement.webkitRequestFullscreen) { - document.documentElement - .webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); - } + || document.msFullscreenElement); }, /** diff --git a/modules/keyboardshortcut/keyboardshortcut.js b/modules/keyboardshortcut/keyboardshortcut.js index 28c7a499f..5d5f556da 100644 --- a/modules/keyboardshortcut/keyboardshortcut.js +++ b/modules/keyboardshortcut/keyboardshortcut.js @@ -90,6 +90,17 @@ const KeyboardShortcut = { enabled = value; }, + /** + * Opens the {@KeyboardShortcutsDialog} dialog. + * + * @returns {void} + */ + openDialog() { + APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, { + shortcutDescriptions: _shortcutsHelp + })); + }, + /** * Registers a new shortcut. * @@ -177,9 +188,7 @@ const KeyboardShortcut = { _initGlobalShortcuts() { this.registerShortcut('?', null, () => { sendAnalytics(createShortcutEvent('help')); - APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, { - shortcutDescriptions: _shortcutsHelp - })); + this.openDialog(); }, 'keyboardShortcuts.toggleShortcuts'); // register SPACE shortcut in two steps to insure visibility of help diff --git a/react/features/base/conference/actionTypes.js b/react/features/base/conference/actionTypes.js index 67845a6b0..9aeaa8a32 100644 --- a/react/features/base/conference/actionTypes.js +++ b/react/features/base/conference/actionTypes.js @@ -96,6 +96,18 @@ export const P2P_STATUS_CHANGED = Symbol('P2P_STATUS_CHANGED'); */ export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY'); +/** + * The type of (redux) action which sets the desktop sharing enabled flag for + * the current conference. + * + * { + * type: SET_DESKTOP_SHARING_ENABLED, + * desktopSharingEnabled: boolean + * } + */ +export const SET_DESKTOP_SHARING_ENABLED + = Symbol('SET_DESKTOP_SHARING_ENABLED'); + /** * The type of (redux) action which updates the current known status of the * Follow Me feature. diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index 09eaac843..9f2f2b455 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -31,6 +31,7 @@ import { LOCK_STATE_CHANGED, P2P_STATUS_CHANGED, SET_AUDIO_ONLY, + SET_DESKTOP_SHARING_ENABLED, SET_FOLLOW_ME, SET_LASTN, SET_PASSWORD, @@ -433,6 +434,22 @@ export function setAudioOnly(audioOnly: boolean) { }; } +/** + * Sets the flag for indicating if desktop sharing is enabled. + * + * @param {boolean} desktopSharingEnabled - True if desktop sharing is enabled. + * @returns {{ + * type: SET_DESKTOP_SHARING_ENABLED, + * desktopSharingEnabled: boolean + * }} + */ +export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) { + return { + type: SET_DESKTOP_SHARING_ENABLED, + desktopSharingEnabled + }; +} + /** * Enables or disables the Follow Me feature. * diff --git a/react/features/base/conference/reducer.js b/react/features/base/conference/reducer.js index 9b4bf9eb1..91790fdde 100644 --- a/react/features/base/conference/reducer.js +++ b/react/features/base/conference/reducer.js @@ -14,6 +14,7 @@ import { LOCK_STATE_CHANGED, P2P_STATUS_CHANGED, SET_AUDIO_ONLY, + SET_DESKTOP_SHARING_ENABLED, SET_FOLLOW_ME, SET_PASSWORD, SET_RECEIVE_VIDEO_QUALITY, @@ -57,6 +58,9 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => { case SET_AUDIO_ONLY: return _setAudioOnly(state, action); + case SET_DESKTOP_SHARING_ENABLED: + return _setDesktopSharingEnabled(state, action); + case SET_FOLLOW_ME: return { ...state, @@ -329,6 +333,21 @@ function _setAudioOnly(state, action) { return set(state, 'audioOnly', action.audioOnly); } +/** + * Reduces a specific Redux action SET_DESKTOP_SHARING_ENABLED of the feature + * base/conference. + * + * @param {Object} state - The Redux state of the feature base/conference. + * @param {Action} action - The Redux action SET_DESKTOP_SHARING_ENABLED to + * reduce. + * @private + * @returns {Object} The new state of the feature base/conference after the + * reduction of the specified action. + */ +function _setDesktopSharingEnabled(state, action) { + return set(state, 'desktopSharingEnabled', action.desktopSharingEnabled); +} + /** * Reduces a specific Redux action SET_PASSWORD of the feature base/conference. * diff --git a/react/features/base/connection/actions.web.js b/react/features/base/connection/actions.web.js index 7635391e9..69c65946f 100644 --- a/react/features/base/connection/actions.web.js +++ b/react/features/base/connection/actions.web.js @@ -1,4 +1,4 @@ -/* @flow */ +// @flow import type { Dispatch } from 'redux'; @@ -54,10 +54,12 @@ export function connect() { /** * Closes connection. * + * @param {boolean} [requestFeedback] - Whether or not to attempt showing a + * request for call feedback. * @returns {Function} */ -export function disconnect() { +export function disconnect(requestFeedback: boolean = false) { // XXX For web based version we use conference hanging up logic from the old // app. - return () => APP.conference.hangup(); + return () => APP.conference.hangup(requestFeedback); } diff --git a/react/features/base/dialog/components/StatelessDialog.web.js b/react/features/base/dialog/components/StatelessDialog.web.js index 49aa4790f..f00be7519 100644 --- a/react/features/base/dialog/components/StatelessDialog.web.js +++ b/react/features/base/dialog/components/StatelessDialog.web.js @@ -39,6 +39,12 @@ type Props = { */ disableBlanketClickDismiss: boolean, + /** + * If true, the cancel button will not display but cancel actions, like + * clicking the blanket, will cancel. + */ + hideCancelButton: boolean, + /** * Whether the dialog is modal. This means clicking on the blanket will * leave the dialog open. No cancel button. @@ -263,7 +269,9 @@ class StatelessDialog extends Component { * not modal. */ _renderCancelButton(options = {}) { - if (options.cancelDisabled || options.isModal) { + if (options.cancelDisabled + || options.isModal + || options.hideCancelButton) { return null; } diff --git a/react/features/base/font-icons/jitsi.json b/react/features/base/font-icons/jitsi.json index 52f079d3f..7c30e352f 100755 --- a/react/features/base/font-icons/jitsi.json +++ b/react/features/base/font-icons/jitsi.json @@ -1,6 +1,33 @@ { "IcoMoonType": "selection", "icons": [ + { + "icon": { + "paths": [ + "M598 128h298v298h-86v-152l-418 418-60-60 418-418h-152v-86zM810 810v-298h86v298c0 46-40 86-86 86h-596c-48 0-86-40-86-86v-596c0-46 38-86 86-86h298v86h-298v596h596z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "open_in_new" + ], + "defaultCode": 59550, + "grid": 24 + }, + "attrs": [], + "properties": { + "ligatures": "launch, open_in_new", + "id": 761, + "order": 932, + "prevSize": 24, + "name": "open_in_new", + "code": 59550 + }, + "setIdx": 0, + "setId": 1, + "iconIdx": 0 + }, { "icon": { "paths": [ @@ -18,15 +45,15 @@ "attrs": [], "properties": { "ligatures": "history, restore", - "id": 385, - "order": 930, + "id": 762, + "order": 933, "prevSize": 24, "code": 59571, "name": "restore" }, "setIdx": 0, - "setId": 2, - "iconIdx": 385 + "setId": 1, + "iconIdx": 1 }, { "icon": { @@ -45,15 +72,15 @@ "attrs": [], "properties": { "ligatures": "chevron_right, navigate_next", - "id": 153, - "order": 927, + "id": 763, + "order": 934, "prevSize": 24, "code": 58377, "name": "navigate_next" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 0 + "iconIdx": 2 }, { "icon": { @@ -72,15 +99,15 @@ "attrs": [], "properties": { "ligatures": "menu", - "id": 489, - "order": 926, + "id": 764, + "order": 935, "prevSize": 24, "code": 58834, "name": "menu" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 1 + "iconIdx": 3 }, { "icon": { @@ -99,15 +126,15 @@ "attrs": [], "properties": { "ligatures": "arrow_back", - "id": 45, - "order": 924, + "id": 765, + "order": 936, "prevSize": 24, "code": 58820, "name": "arrow_back" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 2 + "iconIdx": 4 }, { "icon": { @@ -126,15 +153,15 @@ "attrs": [], "properties": { "ligatures": "chevron_left, navigate_before", - "id": 152, - "order": 923, + "id": 766, + "order": 937, "prevSize": 24, "code": 58376, "name": "navigate_before" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 3 + "iconIdx": 5 }, { "icon": { @@ -153,15 +180,15 @@ "attrs": [], "properties": { "ligatures": "public", - "id": 605, - "order": 920, + "id": 767, + "order": 938, "prevSize": 24, "code": 59403, "name": "public" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 4 + "iconIdx": 6 }, { "icon": { @@ -180,15 +207,15 @@ "attrs": [], "properties": { "ligatures": "event_note", - "id": 252, - "order": 919, + "id": 768, + "order": 939, "prevSize": 24, "code": 58902, "name": "event_note" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 5 + "iconIdx": 7 }, { "icon": { @@ -207,15 +234,15 @@ "attrs": [], "properties": { "ligatures": "timer", - "id": 760, - "order": 928, + "id": 769, + "order": 940, "prevSize": 24, "code": 58405, "name": "timer" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 6 + "iconIdx": 8 }, { "icon": { @@ -234,15 +261,15 @@ "attrs": [], "properties": { "ligatures": "bluetooth_audio, bluetooth_searching", - "id": 79, - "order": 911, + "id": 770, + "order": 941, "prevSize": 24, "code": 57770, "name": "bluetooth" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 7 + "iconIdx": 9 }, { "icon": { @@ -261,15 +288,15 @@ "attrs": [], "properties": { "ligatures": "headset", - "id": 376, - "order": 910, + "id": 771, + "order": 942, "prevSize": 24, "code": 58128, "name": "headset" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 8 + "iconIdx": 10 }, { "icon": { @@ -288,15 +315,15 @@ "attrs": [], "properties": { "ligatures": "phone_in_talk", - "id": 566, - "order": 912, + "id": 772, + "order": 943, "prevSize": 24, "code": 58909, "name": "phone-talk" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 9 + "iconIdx": 11 }, { "icon": { @@ -315,15 +342,15 @@ "attrs": [], "properties": { "ligatures": "more_vert", - "id": 0, - "order": 897, + "id": 773, + "order": 944, "prevSize": 24, "code": 58836, "name": "thumb-menu" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 10 + "iconIdx": 12 }, { "icon": { @@ -344,15 +371,15 @@ {} ], "properties": { - "order": 850, - "id": 1, + "order": 945, + "id": 774, "name": "ninja", "prevSize": 24, "code": 59657 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 11 + "iconIdx": 13 }, { "icon": { @@ -371,15 +398,15 @@ "attrs": [], "properties": { "ligatures": "call, local_phone, phone", - "id": 2, - "order": 851, + "id": 775, + "order": 946, "prevSize": 24, "code": 57549, "name": "phone" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 12 + "iconIdx": 14 }, { "icon": { @@ -398,15 +425,15 @@ "attrs": [], "properties": { "ligatures": "add", - "id": 3, - "order": 896, + "id": 776, + "order": 947, "prevSize": 24, "code": 57669, "name": "add" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 13 + "iconIdx": 15 }, { "icon": { @@ -427,15 +454,15 @@ {} ], "properties": { - "order": 901, - "id": 0, + "order": 948, + "id": 777, "name": "gsm-bars-black", "prevSize": 32, "code": 59686 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 14 + "iconIdx": 16 }, { "icon": { @@ -456,15 +483,15 @@ {} ], "properties": { - "order": 898, - "id": 0, + "order": 949, + "id": 778, "name": "info", "prevSize": 32, "code": 59682 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 15 + "iconIdx": 17 }, { "icon": { @@ -485,15 +512,15 @@ {} ], "properties": { - "order": 856, - "id": 0, + "order": 950, + "id": 779, "name": "mic-camera-combined", "prevSize": 32, "code": 59651 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 16 + "iconIdx": 18 }, { "icon": { @@ -514,15 +541,15 @@ {} ], "properties": { - "order": 857, - "id": 1, + "order": 951, + "id": 780, "name": "feedback", "prevSize": 32, "code": 59677 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 17 + "iconIdx": 19 }, { "icon": { @@ -543,15 +570,15 @@ {} ], "properties": { - "order": 858, - "id": 2, + "order": 952, + "id": 781, "name": "toggle-filmstrip", "prevSize": 32, "code": 59676 }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 18 + "iconIdx": 20 }, { "icon": { @@ -568,16 +595,16 @@ }, "attrs": [], "properties": { - "id": 3, - "order": 859, + "id": 782, + "order": 953, "ligatures": "account_circle", "prevSize": 32, "code": 59649, "name": "avatar" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 19 + "iconIdx": 21 }, { "icon": { @@ -594,16 +621,16 @@ }, "attrs": [], "properties": { - "id": 4, - "order": 860, + "id": 783, + "order": 954, "ligatures": "call_end", "prevSize": 32, "code": 59653, "name": "hangup" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 20 + "iconIdx": 22 }, { "icon": { @@ -620,16 +647,16 @@ }, "attrs": [], "properties": { - "id": 5, - "order": 861, + "id": 784, + "order": 955, "ligatures": "chat_bubble_outline", "prevSize": 32, "code": 59654, "name": "chat" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 21 + "iconIdx": 23 }, { "icon": { @@ -646,16 +673,16 @@ }, "attrs": [], "properties": { - "id": 6, - "order": 862, + "id": 785, + "order": 956, "ligatures": "cloud_download", "prevSize": 32, "code": 59650, "name": "download" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 22 + "iconIdx": 24 }, { "icon": { @@ -672,16 +699,16 @@ }, "attrs": [], "properties": { - "id": 7, - "order": 863, + "id": 786, + "order": 957, "ligatures": "create, edit, mode_edit", "prevSize": 32, "code": 59655, "name": "edit" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 23 + "iconIdx": 25 }, { "icon": { @@ -698,16 +725,16 @@ }, "attrs": [], "properties": { - "id": 8, - "order": 864, + "id": 787, + "order": 958, "ligatures": "description", "prevSize": 32, "code": 59656, "name": "share-doc" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 24 + "iconIdx": 26 }, { "icon": { @@ -724,16 +751,16 @@ }, "attrs": [], "properties": { - "id": 9, - "order": 865, + "id": 788, + "order": 959, "ligatures": "eject", "prevSize": 32, "code": 59652, "name": "kick" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 25 + "iconIdx": 27 }, { "icon": { @@ -750,16 +777,16 @@ }, "attrs": [], "properties": { - "id": 10, - "order": 922, + "id": 789, + "order": 960, "ligatures": "expand_less", "prevSize": 32, "code": 59679, "name": "menu-up" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 26 + "iconIdx": 28 }, { "icon": { @@ -776,16 +803,16 @@ }, "attrs": [], "properties": { - "id": 11, - "order": 867, + "id": 790, + "order": 961, "ligatures": "expand_more", "prevSize": 32, "code": 59680, "name": "menu-down" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 27 + "iconIdx": 29 }, { "icon": { @@ -802,16 +829,16 @@ }, "attrs": [], "properties": { - "id": 12, - "order": 868, + "id": 791, + "order": 962, "ligatures": "fullscreen", "prevSize": 32, "code": 59659, "name": "full-screen" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 28 + "iconIdx": 30 }, { "icon": { @@ -828,16 +855,16 @@ }, "attrs": [], "properties": { - "id": 13, - "order": 869, + "id": 792, + "order": 963, "ligatures": "fullscreen_exit", "prevSize": 32, "code": 59660, "name": "exit-full-screen" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 29 + "iconIdx": 31 }, { "icon": { @@ -854,16 +881,16 @@ }, "attrs": [], "properties": { - "id": 14, - "order": 870, + "id": 793, + "order": 964, "ligatures": "grade, star", "prevSize": 32, "code": 59658, "name": "star-full" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 30 + "iconIdx": 32 }, { "icon": { @@ -880,16 +907,16 @@ }, "attrs": [], "properties": { - "id": 15, - "order": 871, + "id": 794, + "order": 965, "ligatures": "lock_open", "prevSize": 32, "code": 59661, "name": "security" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 31 + "iconIdx": 33 }, { "icon": { @@ -906,16 +933,16 @@ }, "attrs": [], "properties": { - "id": 16, - "order": 872, + "id": 795, + "order": 966, "ligatures": "lock_outline", "prevSize": 32, "code": 59662, "name": "security-locked" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 32 + "iconIdx": 34 }, { "icon": { @@ -932,16 +959,16 @@ }, "attrs": [], "properties": { - "id": 17, - "order": 873, + "id": 796, + "order": 967, "ligatures": "loop, sync", "prevSize": 32, "code": 59663, "name": "reload" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 33 + "iconIdx": 35 }, { "icon": { @@ -958,16 +985,16 @@ }, "attrs": [], "properties": { - "id": 18, - "order": 874, + "id": 797, + "order": 968, "ligatures": "mic", "prevSize": 32, "code": 59664, "name": "microphone" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 34 + "iconIdx": 36 }, { "icon": { @@ -984,16 +1011,16 @@ }, "attrs": [], "properties": { - "id": 19, - "order": 875, + "id": 798, + "order": 969, "ligatures": "mic_none", "prevSize": 32, "code": 59665, "name": "mic-empty" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 35 + "iconIdx": 37 }, { "icon": { @@ -1010,16 +1037,16 @@ }, "attrs": [], "properties": { - "id": 20, - "order": 876, + "id": 799, + "order": 970, "ligatures": "mic_off", "prevSize": 32, "code": 59666, "name": "mic-disabled" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 36 + "iconIdx": 38 }, { "icon": { @@ -1036,16 +1063,16 @@ }, "attrs": [], "properties": { - "id": 21, - "order": 899, + "id": 800, + "order": 971, "ligatures": "pan_tool", "prevSize": 32, "code": 59678, "name": "raised-hand" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 37 + "iconIdx": 39 }, { "icon": { @@ -1062,16 +1089,16 @@ }, "attrs": [], "properties": { - "id": 22, - "order": 878, + "id": 801, + "order": 972, "ligatures": "people_outline", "prevSize": 32, "code": 59675, "name": "contactList" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 38 + "iconIdx": 40 }, { "icon": { @@ -1088,16 +1115,16 @@ }, "attrs": [], "properties": { - "id": 23, - "order": 879, + "id": 802, + "order": 973, "ligatures": "person_add", "prevSize": 32, "code": 59667, "name": "link" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 39 + "iconIdx": 41 }, { "icon": { @@ -1114,16 +1141,16 @@ }, "attrs": [], "properties": { - "id": 24, - "order": 880, + "id": 803, + "order": 974, "ligatures": "play_circle_outline", "prevSize": 32, "code": 59668, "name": "shared-video" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 40 + "iconIdx": 42 }, { "icon": { @@ -1140,16 +1167,16 @@ }, "attrs": [], "properties": { - "id": 25, - "order": 881, + "id": 804, + "order": 975, "ligatures": "settings", "prevSize": 32, "code": 59669, "name": "settings" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 41 + "iconIdx": 43 }, { "icon": { @@ -1166,16 +1193,16 @@ }, "attrs": [], "properties": { - "id": 26, - "order": 882, + "id": 805, + "order": 976, "ligatures": "star_border", "prevSize": 32, "code": 59670, "name": "star" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 42 + "iconIdx": 44 }, { "icon": { @@ -1192,16 +1219,16 @@ }, "attrs": [], "properties": { - "id": 27, - "order": 883, + "id": 806, + "order": 977, "ligatures": "switch_camera", "prevSize": 32, "code": 59681, "name": "switch-camera" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 43 + "iconIdx": 45 }, { "icon": { @@ -1218,16 +1245,16 @@ }, "attrs": [], "properties": { - "id": 28, - "order": 884, + "id": 807, + "order": 978, "ligatures": "tv", "prevSize": 32, "code": 59671, "name": "share-desktop" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 44 + "iconIdx": 46 }, { "icon": { @@ -1244,16 +1271,16 @@ }, "attrs": [], "properties": { - "id": 29, - "order": 885, + "id": 808, + "order": 979, "ligatures": "videocam", "prevSize": 32, "code": 59672, "name": "camera" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 45 + "iconIdx": 47 }, { "icon": { @@ -1270,16 +1297,16 @@ }, "attrs": [], "properties": { - "id": 30, - "order": 886, + "id": 809, + "order": 980, "ligatures": "videocam_off", "prevSize": 32, "code": 59673, "name": "camera-disabled" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 46 + "iconIdx": 48 }, { "icon": { @@ -1296,16 +1323,16 @@ }, "attrs": [], "properties": { - "id": 31, - "order": 887, + "id": 810, + "order": 981, "ligatures": "volume_up", "prevSize": 32, "code": 59674, "name": "volume" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 47 + "iconIdx": 49 }, { "icon": { @@ -1325,16 +1352,16 @@ }, "attrs": [], "properties": { - "order": 890, - "id": 34, + "order": 982, + "id": 811, "prevSize": 32, "code": 58899, "name": "recDisable", "ligatures": "" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 48 + "iconIdx": 50 }, { "icon": { @@ -1355,16 +1382,16 @@ }, "attrs": [], "properties": { - "order": 891, - "id": 35, + "order": 983, + "id": 812, "prevSize": 32, "code": 58900, "name": "recEnable", "ligatures": "" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 49 + "iconIdx": 51 }, { "icon": { @@ -1385,16 +1412,16 @@ }, "attrs": [], "properties": { - "order": 892, - "id": 36, + "order": 984, + "id": 813, "prevSize": 32, "code": 58883, "name": "presentation", "ligatures": "" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 50 + "iconIdx": 52 }, { "icon": { @@ -1411,16 +1438,16 @@ }, "attrs": [], "properties": { - "order": 893, + "order": 985, "ligatures": "dialpad", - "id": 37, + "id": 814, "prevSize": 32, "code": 59685, "name": "dialpad" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 51 + "iconIdx": 53 }, { "icon": { @@ -1437,16 +1464,16 @@ }, "attrs": [], "properties": { - "order": 894, + "order": 986, "ligatures": "remove_red_eye, visibility", - "id": 38, + "id": 815, "prevSize": 32, "code": 59683, "name": "visibility" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 52 + "iconIdx": 54 }, { "icon": { @@ -1463,16 +1490,16 @@ }, "attrs": [], "properties": { - "order": 895, + "order": 987, "ligatures": "visibility_off", - "id": 39, + "id": 816, "prevSize": 32, "code": 59684, "name": "visibility-off" }, - "setIdx": 1, + "setIdx": 0, "setId": 1, - "iconIdx": 53 + "iconIdx": 55 } ], "height": 1024, diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index a56c6afa0..2ce0f728b 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -1,4 +1,4 @@ -/* @flow */ +// @flow import UIEvents from '../../../../service/UI/UIEvents'; @@ -10,8 +10,12 @@ import { import { MiddlewareRegistry } from '../redux'; import { playSound, registerSound, unregisterSound } from '../sounds'; -import { localParticipantIdChanged } from './actions'; import { + localParticipantIdChanged, + participantUpdated +} from './actions'; +import { + DOMINANT_SPEAKER_CHANGED, KICK_PARTICIPANT, MUTE_REMOTE_PARTICIPANT, PARTICIPANT_DISPLAY_NAME_CHANGED, @@ -27,6 +31,7 @@ import { import { getAvatarURLByParticipantId, getLocalParticipant, + getParticipantById, getParticipantCount } from './functions'; import { @@ -47,7 +52,7 @@ MiddlewareRegistry.register(store => next => action => { const { conference } = store.getState()['features/base/conference']; if (action.type === PARTICIPANT_JOINED - || action.type === PARTICIPANT_LEFT) { + || action.type === PARTICIPANT_LEFT) { _maybePlaySounds(store, action); } @@ -66,6 +71,27 @@ MiddlewareRegistry.register(store => next => action => { store.dispatch(localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID)); break; + case DOMINANT_SPEAKER_CHANGED: { + // Ensure the raised hand state is cleared for the dominant speaker. + const participant = getLocalParticipant(store.getState()); + + if (participant) { + const local = participant.id === action.participant.id; + + store.dispatch(participantUpdated({ + id: action.participant.id, + local, + raisedHand: false + })); + } + + if (typeof APP === 'object') { + APP.UI.markDominantSpeaker(action.participant.id); + } + + break; + } + case KICK_PARTICIPANT: conference.kickParticipant(action.id); break; @@ -90,10 +116,37 @@ MiddlewareRegistry.register(store => next => action => { case PARTICIPANT_JOINED: case PARTICIPANT_UPDATED: { - if (typeof APP !== 'undefined') { - const participant = action.participant; - const { id, local } = participant; + const { participant } = action; + const { id, local, raisedHand } = participant; + // Send an external update of the local participant's raised hand state + // if a new raised hand state is defined in the action. + if (typeof raisedHand !== 'undefined') { + if (local) { + conference.setLocalParticipantProperty( + 'raisedHand', + raisedHand); + } + + if (typeof APP === 'object') { + if (local) { + APP.UI.onLocalRaiseHandChanged(raisedHand); + APP.UI.setLocalRaisedHandStatus(raisedHand); + } else { + const remoteParticipant + = getParticipantById(store.getState(), id); + + remoteParticipant + && APP.UI.setRaisedHandStatus( + remoteParticipant.id, + remoteParticipant.name, + raisedHand); + } + } + } + + // Notify external listeners of potential avatarURL changes. + if (typeof APP === 'object') { const preUpdateAvatarURL = getAvatarURLByParticipantId(store.getState(), id); diff --git a/react/features/base/tracks/actionTypes.js b/react/features/base/tracks/actionTypes.js index 2a8351cdb..bb2ef9fef 100644 --- a/react/features/base/tracks/actionTypes.js +++ b/react/features/base/tracks/actionTypes.js @@ -1,3 +1,13 @@ +/** + * The type of redux action dispatched to disable screensharing or to start the + * flow for enabling screenshare. + * + * { + * type: TOGGLE_SCREENSHARING + * } + */ +export const TOGGLE_SCREENSHARING = Symbol('TOGGLE_SCREENSHARING'); + /** * The type of redux action dispatched when a track has been (locally or * remotely) added to the conference. diff --git a/react/features/base/tracks/actions.js b/react/features/base/tracks/actions.js index 076c399b7..dce735b16 100644 --- a/react/features/base/tracks/actions.js +++ b/react/features/base/tracks/actions.js @@ -12,6 +12,7 @@ import { import { getLocalParticipant } from '../participants'; import { + TOGGLE_SCREENSHARING, TRACK_ADDED, TRACK_CREATE_CANCELED, TRACK_CREATE_ERROR, @@ -172,6 +173,20 @@ export function destroyLocalTracks() { }; } +/** + * Signals that the local participant is ending screensharing or beginning the + * screensharing flow. + * + * @returns {{ + * type: TOGGLE_SCREENSHARING, + * }} + */ +export function toggleScreensharing() { + return { + type: TOGGLE_SCREENSHARING + }; +} + /** * Replaces one track with another for one renegotiation instead of invoking * two renegotiations with a separate removeTrack and addTrack. Disposes the diff --git a/react/features/base/tracks/middleware.js b/react/features/base/tracks/middleware.js index c9e1fab20..1da33a3e7 100644 --- a/react/features/base/tracks/middleware.js +++ b/react/features/base/tracks/middleware.js @@ -1,4 +1,4 @@ -/* @flow */ +// @flow import { CAMERA_FACING_MODE, @@ -10,9 +10,15 @@ import { toggleCameraFacingMode } from '../media'; import { MiddlewareRegistry } from '../redux'; +import UIEvents from '../../../../service/UI/UIEvents'; import { createLocalTracksA } from './actions'; -import { TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from './actionTypes'; +import { + TOGGLE_SCREENSHARING, + TRACK_ADDED, + TRACK_REMOVED, + TRACK_UPDATED +} from './actionTypes'; import { getLocalTrack, setTrackMuted } from './functions'; declare var APP: Object; @@ -81,6 +87,12 @@ MiddlewareRegistry.register(store => next => action => { break; } + case TOGGLE_SCREENSHARING: + if (typeof APP === 'object') { + APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING); + } + break; + case TRACK_ADDED: // TODO Remove this middleware case once all UI interested in new tracks // being added are converted to react and listening for store changes. diff --git a/react/features/chat/actionTypes.js b/react/features/chat/actionTypes.js new file mode 100644 index 000000000..0d489648f --- /dev/null +++ b/react/features/chat/actionTypes.js @@ -0,0 +1,23 @@ +/** + * The type of the action which signals to add a new chat message. + * + * { + * type: ADD_MESSAGE, + * hasRead: boolean, + * message: string, + * timestamp: string, + * userName: string + * } + */ +export const ADD_MESSAGE = Symbol('ADD_MESSAGE'); + +/** + * The type of the action which updates which is the most recent message that + * has been seen by the local participant. + * + * { + * type: SET_LAST_READ_MESSAGE, + * message: Object + * } + */ +export const SET_LAST_READ_MESSAGE = Symbol('SET_LAST_READ_MESSAGE'); diff --git a/react/features/chat/actions.js b/react/features/chat/actions.js new file mode 100644 index 000000000..d57826ed7 --- /dev/null +++ b/react/features/chat/actions.js @@ -0,0 +1,63 @@ +import { ADD_MESSAGE, SET_LAST_READ_MESSAGE } from './actionTypes'; + +/* eslint-disable max-params */ + +/** + * Adds a chat message to the collection of messages. + * + * @param {string} userName - The username to display of the participant that + * authored the message. + * @param {string} message - The received message to display. + * @param {string} timestamp - A timestamp to display for when the message was + * received. + * @param {boolean} hasRead - Whether or not to immediately mark the message as + * read. + * @returns {{ + * type: ADD_MESSAGE, + * hasRead: boolean, + * message: string, + * timestamp: string, + * userName: string + * }} + */ +export function addMessage(userName, message, timestamp, hasRead) { + return { + type: ADD_MESSAGE, + hasRead, + message, + timestamp, + userName + }; +} + +/* eslint-enable max-params */ + +/** + * Sets the last read message cursor to the latest message. + * + * @returns {Function} + */ +export function markAllRead() { + return (dispatch, getState) => { + const { messages } = getState()['features/chat']; + + dispatch(setLastReadMessage(messages[messages.length - 1])); + }; +} + +/** + * Updates the last read message cursor to be set at the passed in message. The + * assumption is that messages will be ordered chronologically. + * + * @param {Object} message - The message from the redux state. + * @returns {{ + * type: SET_LAST_READ_MESSAGE, + * message: Object + * }} + */ +export function setLastReadMessage(message) { + return { + type: SET_LAST_READ_MESSAGE, + message + }; +} diff --git a/react/features/chat/components/ChatCounter.native.js b/react/features/chat/components/ChatCounter.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/chat/components/ChatCounter.web.js b/react/features/chat/components/ChatCounter.web.js new file mode 100644 index 000000000..79984eec3 --- /dev/null +++ b/react/features/chat/components/ChatCounter.web.js @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { getUnreadCount } from '../functions'; + +/** + * FIXME: Move this UI logic to a generic component that can be used for + * {@code ParticipantCounter} as well. + */ + +/** + * Implements a React {@link Component} which displays a count of the number of + * unread chat messages. + * + * @extends Component + */ +class ChatCounter extends Component { + static propTypes = { + /** + * The number of unread chat messages in the conference. + */ + _count: PropTypes.number + }; + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + return ( + + + { this.props._count || null } + + + ); + } +} + +/** + * Maps (parts of) the Redux state to the associated {@code ChatCounter}'s + * props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _count: number + * }} + */ +function _mapStateToProps(state) { + return { + _count: getUnreadCount(state) + }; +} + +export default connect(_mapStateToProps)(ChatCounter); diff --git a/react/features/chat/components/index.js b/react/features/chat/components/index.js new file mode 100644 index 000000000..895d1c530 --- /dev/null +++ b/react/features/chat/components/index.js @@ -0,0 +1 @@ +export ChatCounter from './ChatCounter'; diff --git a/react/features/chat/functions.js b/react/features/chat/functions.js new file mode 100644 index 000000000..bec532819 --- /dev/null +++ b/react/features/chat/functions.js @@ -0,0 +1,20 @@ +// @flow + +/** + * Selector for calculating the number of unread chat messages. + * + * @param {Object} state - The redux state. + * @returns {number} The number of unread messages. + */ +export function getUnreadCount(state: Object) { + const { lastReadMessage, messages } = state['features/chat']; + const messagesCount = messages.length; + + if (!messagesCount) { + return 0; + } + + const lastReadIndex = messages.lastIndexOf(lastReadMessage); + + return messagesCount - (lastReadIndex + 1); +} diff --git a/react/features/chat/index.js b/react/features/chat/index.js index 5d43fae60..2bf260236 100644 --- a/react/features/chat/index.js +++ b/react/features/chat/index.js @@ -1,3 +1,7 @@ +export * from './actions'; +export * from './actionTypes'; +export * from './components'; export * from './constants'; import './middleware'; +import './reducer'; diff --git a/react/features/chat/reducer.js b/react/features/chat/reducer.js new file mode 100644 index 000000000..c084f32f1 --- /dev/null +++ b/react/features/chat/reducer.js @@ -0,0 +1,44 @@ +// @flow + +import { ReducerRegistry } from '../base/redux'; + +import { + ADD_MESSAGE, + SET_LAST_READ_MESSAGE +} from './actionTypes'; + +const DEFAULT_STATE = { + open: false, + messages: [], + lastReadMessage: null +}; + +ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => { + switch (action.type) { + case ADD_MESSAGE: { + const newMessage = { + message: action.message, + timestamp: action.timestamp, + userName: action.userName + }; + + return { + ...state, + lastReadMessage: + action.hasRead ? newMessage : state.lastReadMessage, + messages: [ + ...state.messages, + newMessage + ] + }; + } + + case SET_LAST_READ_MESSAGE: + return { + ...state, + lastReadMessage: action.message + }; + } + + return state; +}); diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js index 6a117d35b..08e55743b 100644 --- a/react/features/conference/components/Conference.web.js +++ b/react/features/conference/components/Conference.web.js @@ -11,7 +11,14 @@ import { CalleeInfoContainer } from '../../base/jwt'; import { Filmstrip } from '../../filmstrip'; import { LargeVideo } from '../../large-video'; import { NotificationsContainer } from '../../notifications'; -import { showToolbox, Toolbox } from '../../toolbox'; +import { SidePanel } from '../../side-panel'; +import { + Toolbox, + ToolboxV2, + fullScreenChanged, + setToolboxAlwaysVisible, + showToolbox +} from '../../toolbox'; import { HideNotificationBarStyle } from '../../unsupported-browser'; import { maybeShowSuboptimalExperienceNotification } from '../functions'; @@ -19,11 +26,29 @@ import { maybeShowSuboptimalExperienceNotification } from '../functions'; declare var APP: Object; declare var interfaceConfig: Object; +/** + * DOM events for when full screen mode has changed. Different browsers need + * different vendor prefixes. + * + * @private + * @type {Array} + */ +const FULL_SCREEN_EVENTS = [ + 'webkitfullscreenchange', + 'mozfullscreenchange', + 'fullscreenchange' +]; + /** * The type of the React {@code Component} props of {@link Conference}. */ type Props = { + /** + * Whether the toolbar should stay visible or be able to autohide. + */ + _alwaysVisibleToolbar: boolean, + /** * Whether the local participant is recording the conference. */ @@ -37,6 +62,7 @@ type Props = { * The conference page of the Web application. */ class Conference extends Component { + _onFullScreenChange: Function; _onShowToolbar: Function; _originalOnShowToolbar: Function; @@ -59,6 +85,9 @@ class Conference extends Component { leading: true, trailing: false }); + + // Bind event handler so it is only bound once for every instance. + this._onFullScreenChange = this._onFullScreenChange.bind(this); } /** @@ -74,10 +103,16 @@ class Conference extends Component { APP.UI.registerListeners(); APP.UI.bindEvents(); - const { dispatch, t } = this.props; + FULL_SCREEN_EVENTS.forEach(name => + document.addEventListener(name, this._onFullScreenChange)); + + const { _alwaysVisibleToolbar, dispatch, t } = this.props; dispatch(connect()); maybeShowSuboptimalExperienceNotification(dispatch, t); + + dispatch(setToolboxAlwaysVisible( + _alwaysVisibleToolbar || interfaceConfig.filmStripOnly)); } /** @@ -90,6 +125,9 @@ class Conference extends Component { APP.UI.unregisterListeners(); APP.UI.unbindEvents(); + FULL_SCREEN_EVENTS.forEach(name => + document.removeEventListener(name, this._onFullScreenChange)); + APP.conference.isJoined() && this.props.dispatch(disconnect()); } @@ -100,12 +138,26 @@ class Conference extends Component { * @returns {ReactElement} */ render() { - const { filmStripOnly, VIDEO_QUALITY_LABEL_DISABLED } = interfaceConfig; + const { + _USE_NEW_TOOLBOX, + VIDEO_QUALITY_LABEL_DISABLED, + filmStripOnly + } = interfaceConfig; const hideVideoQualityLabel = filmStripOnly || VIDEO_QUALITY_LABEL_DISABLED || this.props._iAmRecorder; + let ToolboxToUse; + + if (filmStripOnly) { + ToolboxToUse = null; + } else if (interfaceConfig._USE_NEW_TOOLBOX) { + ToolboxToUse = ToolboxV2; + } else { + ToolboxToUse = Toolbox; + } + return (
{
- { filmStripOnly ? null : } + { ToolboxToUse && } + + { _USE_NEW_TOOLBOX && !filmStripOnly + && } @@ -135,6 +190,17 @@ class Conference extends Component { ); } + /** + * Updates the Redux state when full screen mode has been enabled or + * disabled. + * + * @private + * @returns {void} + */ + _onFullScreenChange() { + this.props.dispatch(fullScreenChanged(APP.UI.isFullScreen())); + } + /** * Displays the toolbar. * @@ -153,17 +219,30 @@ class Conference extends Component { * @param {Object} state - The Redux state. * @private * @returns {{ + * _alwaysVisibleToolbar: boolean, * _iAmRecorder: boolean * }} */ function _mapStateToProps(state) { + const { + alwaysVisibleToolbar, + iAmRecorder + } = state['features/base/config']; + return { + /** + * Whether the toolbar should stay visible or be able to autohide. + * + * @private + */ + _alwaysVisibleToolbar: alwaysVisibleToolbar, + /** * Whether the local participant is recording the conference. * * @private */ - _iAmRecorder: state['features/base/config'].iAmRecorder + _iAmRecorder: iAmRecorder }; } diff --git a/react/features/etherpad/actionTypes.js b/react/features/etherpad/actionTypes.js new file mode 100644 index 000000000..25d009b73 --- /dev/null +++ b/react/features/etherpad/actionTypes.js @@ -0,0 +1,29 @@ +/** + * The type of the action which signals document editing has been enabled. + * + * { + * type: ETHERPAD_INITIALIZED + * } + */ +export const ETHERPAD_INITIALIZED = Symbol('ETHERPAD_INITIALIZED'); + + +/** + * The type of the action which signals document editing has stopped or started. + * + * { + * type: SET_DOCUMENT_EDITING_STATUS + * } + */ +export const SET_DOCUMENT_EDITING_STATUS + = Symbol('SET_DOCUMENT_EDITING_STATUS'); + +/** + * The type of the action which signals to start or stop editing a shared + * document. + * + * { + * type: TOGGLE_DOCUMENT_EDITING + * } + */ +export const TOGGLE_DOCUMENT_EDITING = Symbol('TOGGLE_DOCUMENT_EDITING'); diff --git a/react/features/etherpad/actions.js b/react/features/etherpad/actions.js new file mode 100644 index 000000000..1b3aebc71 --- /dev/null +++ b/react/features/etherpad/actions.js @@ -0,0 +1,50 @@ +// @flow + +import { + ETHERPAD_INITIALIZED, + SET_DOCUMENT_EDITING_STATUS, + TOGGLE_DOCUMENT_EDITING +} from './actionTypes'; + +/** + * Dispatches an action to set whether document editing has started or stopped. + * + * @param {boolean} editing - Whether or not a document is currently being + * edited. + * @returns {{ + * type: SET_DOCUMENT_EDITING_STATUS, + * editing: boolean + * }} + */ +export function setDocumentEditingState(editing: boolean) { + return { + type: SET_DOCUMENT_EDITING_STATUS, + editing + }; +} + +/** + * Dispatches an action to set Etherpad as having been initialized. + * + * @returns {{ + * type: ETHERPAD_INITIALIZED + * }} + */ +export function setEtherpadHasInitialzied() { + return { + type: ETHERPAD_INITIALIZED + }; +} + +/** + * Dispatches an action to show or hide Etherpad. + * + * @returns {{ + * type: TOGGLE_DOCUMENT_EDITING + * }} + */ +export function toggleDocument() { + return { + type: TOGGLE_DOCUMENT_EDITING + }; +} diff --git a/react/features/etherpad/index.js b/react/features/etherpad/index.js new file mode 100644 index 000000000..f3a2aac65 --- /dev/null +++ b/react/features/etherpad/index.js @@ -0,0 +1,5 @@ +export * from './actions'; +export * from './actionTypes'; + +import './middleware'; +import './reducer'; diff --git a/react/features/etherpad/middleware.js b/react/features/etherpad/middleware.js new file mode 100644 index 000000000..c847c7ae1 --- /dev/null +++ b/react/features/etherpad/middleware.js @@ -0,0 +1,30 @@ +// @flow + +import { MiddlewareRegistry } from '../base/redux'; +import UIEvents from '../../../service/UI/UIEvents'; + +import { TOGGLE_DOCUMENT_EDITING } from './actionTypes'; + +declare var APP: Object; + +/** + * Middleware that captures actions related to collaborative document editing + * and notifies components not hooked into redux. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +// eslint-disable-next-line no-unused-vars +MiddlewareRegistry.register(store => next => action => { + if (typeof APP === 'undefined') { + return next(action); + } + + switch (action.type) { + case TOGGLE_DOCUMENT_EDITING: + APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED); + break; + } + + return next(action); +}); diff --git a/react/features/etherpad/reducer.js b/react/features/etherpad/reducer.js new file mode 100644 index 000000000..80ad74be9 --- /dev/null +++ b/react/features/etherpad/reducer.js @@ -0,0 +1,30 @@ +// @flow + +import { ReducerRegistry } from '../base/redux'; + +import { + ETHERPAD_INITIALIZED, + SET_DOCUMENT_EDITING_STATUS +} from './actionTypes'; + +/** + * Reduces the Redux actions of the feature features/etherpad. + */ +ReducerRegistry.register('features/etherpad', (state = {}, action) => { + switch (action.type) { + case ETHERPAD_INITIALIZED: + return { + ...state, + initialized: true + }; + + case SET_DOCUMENT_EDITING_STATUS: + return { + ...state, + editing: action.editing + }; + + default: + return state; + } +}); diff --git a/react/features/filmstrip/components/Filmstrip.web.js b/react/features/filmstrip/components/Filmstrip.web.js index 757c4677a..1240981e6 100644 --- a/react/features/filmstrip/components/Filmstrip.web.js +++ b/react/features/filmstrip/components/Filmstrip.web.js @@ -7,11 +7,13 @@ import { connect } from 'react-redux'; import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants'; import { InviteButton } from '../../invite'; -import { Toolbox } from '../../toolbox'; +import { Toolbox, ToolboxFilmstrip, dockToolbox } from '../../toolbox'; import { setFilmstripHovered } from '../actions'; import { shouldRemoteVideosBeVisible } from '../functions'; +declare var interfaceConfig: Object; + /** * Implements a React {@link Component} which represents the filmstrip on * Web/React. @@ -62,6 +64,12 @@ class Filmstrip extends Component<*> { */ _remoteVideosVisible: PropTypes.bool, + /** + * Whether or not the toolbox is visible. The height of the vertical + * filmstrip needs to adjust to accommodate the horizontal toolbox. + */ + _toolboxVisible: PropTypes.bool, + /** * Updates the redux store with filmstrip hover changes. */ @@ -111,6 +119,7 @@ class Filmstrip extends Component<*> { _isAddToCallAvailable, _isDialOutAvailable, _remoteVideosVisible, + _toolboxVisible, filmstripOnly } = this.props; @@ -122,13 +131,17 @@ class Filmstrip extends Component<*> { * will get updated without replacing the DOM. If the known DOM gets * modified, then the views will get blown away. */ + const reduceHeight + = _toolboxVisible && interfaceConfig.TOOLBAR_BUTTONS.length; + const filmstripClassNames = `filmstrip ${_remoteVideosVisible + ? '' : 'hide-videos'} ${reduceHeight ? 'reduce-height' : ''}`; - const filmstripClassNames = `filmstrip ${_remoteVideosVisible ? '' - : 'hide-videos'}`; + const ToolboxToUse = interfaceConfig._USE_NEW_TOOLBOX + ? ToolboxFilmstrip : Toolbox; return (
- { filmstripOnly ? : null } + { filmstripOnly ? : null }
@@ -172,6 +185,9 @@ class Filmstrip extends Component<*> { */ _notifyOfHoveredStateUpdate() { if (this.props._hovered !== this._isHovered) { + if (interfaceConfig._USE_NEW_TOOLBOX) { + this.props.dispatch(dockToolbox(this._isHovered)); + } this.props.dispatch(setFilmstripHovered(this._isHovered)); } } @@ -211,7 +227,8 @@ class Filmstrip extends Component<*> { * _hovered: boolean, * _isAddToCallAvailable: boolean, * _isDialOutAvailable: boolean, - * _remoteVideosVisible: boolean + * _remoteVideosVisible: boolean, + * _toolboxVisible: boolean * }} */ function _mapStateToProps(state) { @@ -231,11 +248,13 @@ function _mapStateToProps(state) { return { _hideInviteButton: iAmRecorder - || (!isAddToCallAvailable && !isDialOutAvailable), + || (!isAddToCallAvailable && !isDialOutAvailable) + || interfaceConfig._USE_NEW_TOOLBOX, _hovered: hovered, _isAddToCallAvailable: isAddToCallAvailable, _isDialOutAvailable: isDialOutAvailable, - _remoteVideosVisible: shouldRemoteVideosBeVisible(state) + _remoteVideosVisible: shouldRemoteVideosBeVisible(state), + _toolboxVisible: state['features/toolbox'].visible }; } diff --git a/react/features/invite/components/InfoDialogButton.web.js b/react/features/invite/components/InfoDialogButton.web.js index 47fdce682..c656f67e8 100644 --- a/react/features/invite/components/InfoDialogButton.web.js +++ b/react/features/invite/components/InfoDialogButton.web.js @@ -5,9 +5,15 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { ToolbarButton, TOOLTIP_TO_POPUP_POSITION } from '../../toolbox'; +import { createToolbarEvent, sendAnalytics } from '../../analytics'; +import { translate } from '../../base/i18n'; +import { + ToolbarButton, + ToolbarButtonV2, + TOOLTIP_TO_POPUP_POSITION +} from '../../toolbox'; -import { setInfoDialogVisibility } from '../actions'; +import { setInfoDialogVisibility, updateDialInNumbers } from '../actions'; import { InfoDialog } from './info-dialog'; const { INITIAL_TOOLBAR_TIMEOUT } = interfaceConfig; @@ -39,6 +45,15 @@ class InfoDialogButton extends Component { * @static */ static propTypes = { + + /** + * Phone numbers for dialing into the conference. + */ + _dialInNumbers: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array + ]), + /** * Whether or not the {@code InfoDialog} should close by itself after a * a timeout. @@ -61,6 +76,11 @@ class InfoDialogButton extends Component { */ dispatch: PropTypes.func, + /** + * Invoked to obtain translated strings. + */ + t: PropTypes.func, + /** * From which side tooltips should display. Will be re-used for * displaying the inline dialog for video quality adjustment. @@ -100,6 +120,10 @@ class InfoDialogButton extends Component { if (this.props._shouldAutoClose) { this._setAutoCloseTimeout(); } + + if (!this.props._dialInNumbers) { + this.props.dispatch(updateDialInNumbers()); + } } /** @@ -145,29 +169,9 @@ class InfoDialogButton extends Component { * @returns {ReactElement} */ render() { - const { _showDialog, _toolboxVisible, tooltipPosition } = this.props; - const buttonConfiguration = { - ...DEFAULT_BUTTON_CONFIGURATION, - classNames: [ - ...DEFAULT_BUTTON_CONFIGURATION.classNames, - _showDialog ? 'toggled button-active' : '' - ] - }; - - return ( - } - isOpen = { _toolboxVisible && _showDialog } - onClose = { this._onDialogClose } - position = { TOOLTIP_TO_POPUP_POSITION[tooltipPosition] }> - - - ); + return interfaceConfig._USE_NEW_TOOLBOX + ? this._renderNewToolbarButton() + : this._renderOldToolbarButton(); } /** @@ -208,9 +212,75 @@ class InfoDialogButton extends Component { * @returns {void} */ _onDialogToggle() { + sendAnalytics(createToolbarEvent('info')); + this.props.dispatch(setInfoDialogVisibility(!this.props._showDialog)); } + /** + * Renders a React Element for the {@code InfoDialog} using legacy + * {@code ToolbarButton}. + * + * @private + * @returns {ReactElement} + */ + _renderOldToolbarButton() { + const { _showDialog, _toolboxVisible, tooltipPosition } = this.props; + const buttonConfiguration = { + ...DEFAULT_BUTTON_CONFIGURATION, + classNames: [ + ...DEFAULT_BUTTON_CONFIGURATION.classNames, + _showDialog ? 'toggled button-active' : '' + ] + }; + + return ( + } + isOpen = { _toolboxVisible && _showDialog } + onClose = { this._onDialogClose } + position = { TOOLTIP_TO_POPUP_POSITION[tooltipPosition] }> + + + ); + } + + /** + * Renders a React Element for the {@code InfoDialog} using the newer + * {@code ToolbarButtonV2}. + * + * @private + * @returns {ReactElement} + */ + _renderNewToolbarButton() { + const { _showDialog, _toolboxVisible, t } = this.props; + const iconClass = `icon-info ${_showDialog ? 'toggled' : ''}`; + + return ( +
+ } + isOpen = { _toolboxVisible && _showDialog } + onClose = { this._onDialogClose } + position = { 'top right' }> + + +
+ ); + } + /** * Set a timeout to automatically hide the {@code InfoDialog}. * @@ -235,6 +305,7 @@ class InfoDialogButton extends Component { * @param {Object} state - The Redux state. * @private * @returns {{ + * _dialInNumbers: Array, * _shouldAutoClose: boolean, * _showDialog: boolean, * _toolboxVisible: boolean @@ -243,14 +314,16 @@ class InfoDialogButton extends Component { function _mapStateToProps(state) { const { infoDialogVisible, - infoDialogWillAutoClose + infoDialogWillAutoClose, + numbers } = state['features/invite']; return { + _dialInNumbers: numbers, _shouldAutoClose: infoDialogWillAutoClose, _showDialog: infoDialogVisible, _toolboxVisible: state['features/toolbox'].visible }; } -export default connect(_mapStateToProps)(InfoDialogButton); +export default translate(connect(_mapStateToProps)(InfoDialogButton)); diff --git a/react/features/invite/components/info-dialog/InfoDialog.web.js b/react/features/invite/components/info-dialog/InfoDialog.web.js index 7682eb948..41530711c 100644 --- a/react/features/invite/components/info-dialog/InfoDialog.web.js +++ b/react/features/invite/components/info-dialog/InfoDialog.web.js @@ -24,6 +24,15 @@ const logger = require('jitsi-meet-logger').getLogger(__filename); * @extends Component */ class InfoDialog extends Component { + /** + * Default values for {@code InfoDialog} component's properties. + * + * @static + */ + static defaultProps = { + autoUpdateNumbers: true + }; + /** * {@code InfoDialog} component's property types. * @@ -69,6 +78,13 @@ class InfoDialog extends Component { */ _password: PropTypes.string, + /** + * Whether or not this component should make a request for dial-in + * numbers. If false, this component will rely on an outside source + * updating and passing in numbers through the _dialIn prop. + */ + autoUpdateNumbers: PropTypes.bool, + /** * Invoked to open a dialog for adding participants to the conference. */ @@ -148,7 +164,7 @@ class InfoDialog extends Component { * @returns {void} */ componentDidMount() { - if (!this.state.phoneNumber) { + if (!this.state.phoneNumber && this.props.autoUpdateNumbers) { this.props.dispatch(updateDialInNumbers()); } } diff --git a/react/features/keyboard-shortcuts/actionTypes.js b/react/features/keyboard-shortcuts/actionTypes.js new file mode 100644 index 000000000..dbb41746b --- /dev/null +++ b/react/features/keyboard-shortcuts/actionTypes.js @@ -0,0 +1,10 @@ +/** + * The type of the action which signals the keyboard shortcuts dialog should + * be displayed. + * + * { + * type: OPEN_KEYBOARD_SHORTCUTS_DIALOG + * } + */ +export const OPEN_KEYBOARD_SHORTCUTS_DIALOG + = Symbol('OPEN_KEYBOARD_SHORTCUTS_DIALOG'); diff --git a/react/features/keyboard-shortcuts/actions.js b/react/features/keyboard-shortcuts/actions.js new file mode 100644 index 000000000..8611e27de --- /dev/null +++ b/react/features/keyboard-shortcuts/actions.js @@ -0,0 +1,14 @@ +import { OPEN_KEYBOARD_SHORTCUTS_DIALOG } from './actionTypes'; + +/** + * Opens the dialog showing available keyboard shortcuts. + * + * @returns {{ + * type: OPEN_KEYBOARD_SHORTCUTS_DIALOG + * }} + */ +export function openKeyboardShortcutsDialog() { + return { + type: OPEN_KEYBOARD_SHORTCUTS_DIALOG + }; +} diff --git a/react/features/keyboard-shortcuts/index.js b/react/features/keyboard-shortcuts/index.js index 07635cbbc..a640fd3bf 100644 --- a/react/features/keyboard-shortcuts/index.js +++ b/react/features/keyboard-shortcuts/index.js @@ -1 +1,4 @@ +export * from './actions'; export * from './components'; + +import './middleware'; diff --git a/react/features/keyboard-shortcuts/middleware.js b/react/features/keyboard-shortcuts/middleware.js new file mode 100644 index 000000000..951cf63ea --- /dev/null +++ b/react/features/keyboard-shortcuts/middleware.js @@ -0,0 +1,26 @@ +// @flow + +import { MiddlewareRegistry } from '../base/redux'; + +import { OPEN_KEYBOARD_SHORTCUTS_DIALOG } from './actionTypes'; + +declare var APP: Object; + +/** + * Implements the middleware of the feature keyboard-shortcuts. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +// eslint-disable-next-line no-unused-vars +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case OPEN_KEYBOARD_SHORTCUTS_DIALOG: + if (typeof APP === 'object') { + APP.keyboardshortcut.openDialog(); + } + break; + } + + return next(action); +}); diff --git a/react/features/recording/actionTypes.js b/react/features/recording/actionTypes.js index dc85a1cec..e79472bf2 100644 --- a/react/features/recording/actionTypes.js +++ b/react/features/recording/actionTypes.js @@ -20,3 +20,25 @@ export const HIDE_RECORDING_LABEL = Symbol('HIDE_RECORDING_LABEL'); * @public */ export const RECORDING_STATE_UPDATED = Symbol('RECORDING_STATE_UPDATED'); + +/** + * The type of Redux action which updates the current known type of configured + * recording. For example, type "jibri" is used for live streaming. + * + * { + * type: RECORDING_STATE_UPDATED, + * recordingType: string + * } + * @public + */ +export const SET_RECORDING_TYPE = Symbol('SET_RECORDING_TYPE'); + +/** + * The type of Redux action triggers the flow to start or stop recording. + * + * { + * type: TOGGLE_RECORDING + * } + * @public + */ +export const TOGGLE_RECORDING = Symbol('TOGGLE_RECORDING'); diff --git a/react/features/recording/actions.js b/react/features/recording/actions.js index 8b74ae7d6..419662c3c 100644 --- a/react/features/recording/actions.js +++ b/react/features/recording/actions.js @@ -1,4 +1,9 @@ -import { HIDE_RECORDING_LABEL, RECORDING_STATE_UPDATED } from './actionTypes'; +import { + HIDE_RECORDING_LABEL, + RECORDING_STATE_UPDATED, + SET_RECORDING_TYPE, + TOGGLE_RECORDING +} from './actionTypes'; /** * Hides any displayed recording label, regardless of current recording state. @@ -13,6 +18,36 @@ export function hideRecordingLabel() { }; } +/** + * Sets what type of recording service will be used. + * + * @param {string} recordingType - The type of recording service to be used. + * Should be one of the enumerated types in {@link RECORDING_TYPES}. + * @returns {{ + * type: SET_RECORDING_TYPE, + * recordingType: string + * }} + */ +export function setRecordingType(recordingType) { + return { + type: SET_RECORDING_TYPE, + recordingType + }; +} + +/** + * Start or stop recording. + * + * @returns {{ + * type: TOGGLE_RECORDING + * }} + */ +export function toggleRecording() { + return { + type: TOGGLE_RECORDING + }; +} + /** * Updates the redux state for the recording feature. * diff --git a/react/features/recording/constants.js b/react/features/recording/constants.js new file mode 100644 index 000000000..3e76a775f --- /dev/null +++ b/react/features/recording/constants.js @@ -0,0 +1,12 @@ +// @flow + +/** + * Expected supported recording types. JIBRI is known to support live streaming + * whereas JIRECON is for recording. + * + * @type {Object} + */ +export const RECORDING_TYPES = { + JIBRI: 'jibri', + JIRECON: 'jirecon' +}; diff --git a/react/features/recording/index.js b/react/features/recording/index.js index 582e1f9dd..c2bba6b0a 100644 --- a/react/features/recording/index.js +++ b/react/features/recording/index.js @@ -1,4 +1,6 @@ export * from './actions'; export * from './components'; +export * from './constants'; +import './middleware'; import './reducer'; diff --git a/react/features/recording/middleware.js b/react/features/recording/middleware.js new file mode 100644 index 000000000..15041ed81 --- /dev/null +++ b/react/features/recording/middleware.js @@ -0,0 +1,27 @@ +// @flow + +import { MiddlewareRegistry } from '../base/redux'; +import UIEvents from '../../../service/UI/UIEvents'; + +import { TOGGLE_RECORDING } from './actionTypes'; + +declare var APP: Object; + +/** + * Implements the middleware of the feature recording. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +// eslint-disable-next-line no-unused-vars +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case TOGGLE_RECORDING: + if (typeof APP === 'object') { + APP.UI.emitEvent(UIEvents.TOGGLE_RECORDING); + } + break; + } + + return next(action); +}); diff --git a/react/features/recording/reducer.js b/react/features/recording/reducer.js index 16406ed9d..188f866e5 100644 --- a/react/features/recording/reducer.js +++ b/react/features/recording/reducer.js @@ -1,5 +1,9 @@ import { ReducerRegistry } from '../base/redux'; -import { HIDE_RECORDING_LABEL, RECORDING_STATE_UPDATED } from './actionTypes'; +import { + HIDE_RECORDING_LABEL, + RECORDING_STATE_UPDATED, + SET_RECORDING_TYPE +} from './actionTypes'; /** * Reduces the Redux actions of the feature features/recording. @@ -18,6 +22,12 @@ ReducerRegistry.register('features/recording', (state = {}, action) => { ...action.recordingState }; + case SET_RECORDING_TYPE: + return { + ...state, + recordingType: action.recordingType + }; + default: return state; } diff --git a/react/features/shared-video/actionTypes.js b/react/features/shared-video/actionTypes.js new file mode 100644 index 000000000..6663a4e74 --- /dev/null +++ b/react/features/shared-video/actionTypes.js @@ -0,0 +1,20 @@ +/** + * The type of the action which signals to update the current known state of the + * shared YouTube video. + * + * { + * type: SET_SHARED_VIDEO_STATUS, + * status: string + * } + */ +export const SET_SHARED_VIDEO_STATUS = Symbol('SET_SHARED_VIDEO_STATUS'); + +/** + * The type of the action which signals to start the flow for starting or + * stopping a shared YouTube video. + * + * { + * type: TOGGLE_SHARED_VIDEO + * } + */ +export const TOGGLE_SHARED_VIDEO = Symbol('TOGGLE_SHARED_VIDEO'); diff --git a/react/features/shared-video/actions.js b/react/features/shared-video/actions.js new file mode 100644 index 000000000..1f5a9389e --- /dev/null +++ b/react/features/shared-video/actions.js @@ -0,0 +1,31 @@ +import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO } from './actionTypes'; + +/** + * Updates the current known status of the shared YouTube video. + * + * @param {string} status - The current status of the YouTube video being + * shared. + * @returns {{ + * type: SET_SHARED_VIDEO_STATUS, + * status: string + * }} + */ +export function setSharedVideoStatus(status) { + return { + type: SET_SHARED_VIDEO_STATUS, + status + }; +} + +/** + * Starts the flow for starting or stopping a shared YouTube video. + * + * @returns {{ + * type: TOGGLE_SHARED_VIDEO + * }} + */ +export function toggleSharedVideo() { + return { + type: TOGGLE_SHARED_VIDEO + }; +} diff --git a/react/features/shared-video/index.js b/react/features/shared-video/index.js new file mode 100644 index 000000000..f3a2aac65 --- /dev/null +++ b/react/features/shared-video/index.js @@ -0,0 +1,5 @@ +export * from './actions'; +export * from './actionTypes'; + +import './middleware'; +import './reducer'; diff --git a/react/features/shared-video/middleware.js b/react/features/shared-video/middleware.js new file mode 100644 index 000000000..74669ceb5 --- /dev/null +++ b/react/features/shared-video/middleware.js @@ -0,0 +1,31 @@ +// @flow + +import UIEvents from '../../../service/UI/UIEvents'; + +import { MiddlewareRegistry } from '../base/redux'; + +import { TOGGLE_SHARED_VIDEO } from './actionTypes'; + +declare var APP: Object; + +/** + * Middleware that captures actions related to YouTube video sharing and updates + * components not hooked into redux. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +// eslint-disable-next-line no-unused-vars +MiddlewareRegistry.register(store => next => action => { + if (typeof APP === 'undefined') { + return next(action); + } + + switch (action.type) { + case TOGGLE_SHARED_VIDEO: + APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED); + break; + } + + return next(action); +}); diff --git a/react/features/shared-video/reducer.js b/react/features/shared-video/reducer.js new file mode 100644 index 000000000..5222f03f5 --- /dev/null +++ b/react/features/shared-video/reducer.js @@ -0,0 +1,19 @@ +import { ReducerRegistry } from '../base/redux'; + +import { SET_SHARED_VIDEO_STATUS } from './actionTypes'; + +/** + * Reduces the Redux actions of the feature features/shared-video. + */ +ReducerRegistry.register('features/shared-video', (state = {}, action) => { + switch (action.type) { + case SET_SHARED_VIDEO_STATUS: + return { + ...state, + status: action.status + }; + + default: + return state; + } +}); diff --git a/react/features/side-panel/actionTypes.js b/react/features/side-panel/actionTypes.js new file mode 100644 index 000000000..33f98934a --- /dev/null +++ b/react/features/side-panel/actionTypes.js @@ -0,0 +1,49 @@ +/** + * The type of the action which signals to close the side panel. + * + * { + * type: CLOSE_PANEL, + * } + */ +export const CLOSE_PANEL = Symbol('CLOSE_PANEL'); + +/** + * The type of the action which to set the name of the current panel being + * displayed in the side panel. + * + * { + * type: SET_VISIBLE_PANEL, + * current: string|null + * } + */ +export const SET_VISIBLE_PANEL = Symbol('SET_VISIBLE_PANEL'); + +/** + * The type of the action which signals to toggle the display of chat in the + * side panel. + * + * { + * type: TOGGLE_CHAT + * } + */ +export const TOGGLE_CHAT = Symbol('TOGGLE_CHAT'); + +/** + * The type of the action which signals to toggle the display of profile editing + * in the side panel. + * + * { + * type: TOGGLE_PROFILE + * } + */ +export const TOGGLE_PROFILE = Symbol('TOGGLE_PROFILE'); + +/** + * The type of the action which signals to toggle the display of settings in the + * side panel. + * + * { + * type: TOGGLE_SETTINGS + * } + */ +export const TOGGLE_SETTINGS = Symbol('TOGGLE_SETTINGS'); diff --git a/react/features/side-panel/actions.js b/react/features/side-panel/actions.js new file mode 100644 index 000000000..b808a12f1 --- /dev/null +++ b/react/features/side-panel/actions.js @@ -0,0 +1,77 @@ +import { + CLOSE_PANEL, + SET_VISIBLE_PANEL, + TOGGLE_CHAT, + TOGGLE_PROFILE, + TOGGLE_SETTINGS +} from './actionTypes'; + +/** + * Dispatches an action to close the currently displayed side panel. + * + * @returns {Function} + */ +export function closePanel() { + return (dispatch, getState) => { + dispatch({ + type: CLOSE_PANEL, + current: getState()['features/side-panel'].current + }); + }; +} + +/** + * Updates the redux store with the currently displayed side panel. + * + * @param {string|null} name - The name of the side panel being displayed. Null + * (or falsy) should be set if no side panel is being displayed. + * @returns {{ + * type: SET_VISIBLE_PANEL, + * current: string + * }} + */ +export function setVisiblePanel(name = null) { + return { + type: SET_VISIBLE_PANEL, + current: name + }; +} + +/** + * Toggles display of the chat side panel. + * + * @returns {{ + * type: TOGGLE_CHAT + * }} + */ +export function toggleChat() { + return { + type: TOGGLE_CHAT + }; +} + +/** + * Toggles display of the profile side panel. + * + * @returns {{ + * type: TOGGLE_PROFILE + * }} + */ +export function toggleProfile() { + return { + type: TOGGLE_PROFILE + }; +} + +/** + * Toggles display of the settings side panel. + * + * @returns {{ + * type: TOGGLE_SETTINGS + * }} + */ +export function toggleSettings() { + return { + type: TOGGLE_SETTINGS + }; +} diff --git a/react/features/side-panel/components/SidePanel.native.js b/react/features/side-panel/components/SidePanel.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/side-panel/components/SidePanel.web.js b/react/features/side-panel/components/SidePanel.web.js new file mode 100644 index 000000000..7208692af --- /dev/null +++ b/react/features/side-panel/components/SidePanel.web.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { closePanel } from '../actions'; + +/** + * React Component for holding features in a side panel that slides in and out. + * + * @extends Component + */ +class SidePanel extends Component { + /** + * {@code SidePanel} component's property types. + * + * @static + */ + static propTypes = { + dispatch: PropTypes.func + }; + + /** + * Initializes a new {@code SidePanel} instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props) { + super(props); + + this._onCloseClick = this._onCloseClick.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + return ( +
+
+ X +
+
+ ); + } + + /** + * Callback invoked to hide {@code SidePanel}. + * + * @returns {void} + */ + _onCloseClick() { + this.props.dispatch(closePanel()); + } +} + +export default connect()(SidePanel); diff --git a/react/features/side-panel/components/index.js b/react/features/side-panel/components/index.js new file mode 100644 index 000000000..420b02391 --- /dev/null +++ b/react/features/side-panel/components/index.js @@ -0,0 +1 @@ +export { default as SidePanel } from './SidePanel'; diff --git a/react/features/side-panel/index.js b/react/features/side-panel/index.js new file mode 100644 index 000000000..a29aa08e0 --- /dev/null +++ b/react/features/side-panel/index.js @@ -0,0 +1,6 @@ +export * from './actions'; +export * from './actionTypes'; +export * from './components'; + +import './middleware'; +import './reducer'; diff --git a/react/features/side-panel/middleware.js b/react/features/side-panel/middleware.js new file mode 100644 index 000000000..828d1dc06 --- /dev/null +++ b/react/features/side-panel/middleware.js @@ -0,0 +1,45 @@ +// @flow + +import { MiddlewareRegistry } from '../base/redux'; + +import { + CLOSE_PANEL, + TOGGLE_CHAT, + TOGGLE_PROFILE, + TOGGLE_SETTINGS +} from './actionTypes'; + +declare var APP: Object; + +/** + * Middleware that catches actions related to the non-reactified web side panel. + * + * @param {Store} store - Redux store. + * @returns {Function} + */ +// eslint-disable-next-line no-unused-vars +MiddlewareRegistry.register(store => next => action => { + if (typeof APP !== 'object') { + return next(action); + } + + switch (action.type) { + case CLOSE_PANEL: + APP.UI.toggleSidePanel(action.current); + break; + + case TOGGLE_CHAT: + APP.UI.toggleChat(); + break; + + case TOGGLE_PROFILE: + APP.UI.toggleSidePanel('profile_container'); + break; + + case TOGGLE_SETTINGS: + APP.UI.toggleSidePanel('settings_container'); + break; + } + + return next(action); +}); diff --git a/react/features/side-panel/reducer.js b/react/features/side-panel/reducer.js new file mode 100644 index 000000000..626856fa7 --- /dev/null +++ b/react/features/side-panel/reducer.js @@ -0,0 +1,18 @@ +import { ReducerRegistry } from '../base/redux'; + +import { SET_VISIBLE_PANEL } from './actionTypes'; + +/** + * Reduces the Redux actions of the feature features/side-panel. + */ +ReducerRegistry.register('features/side-panel', (state = {}, action) => { + switch (action.type) { + case SET_VISIBLE_PANEL: + return { + ...state, + current: action.current + }; + } + + return state; +}); diff --git a/react/features/toolbox/actionTypes.js b/react/features/toolbox/actionTypes.js index 7ed2be5ec..521b77d73 100644 --- a/react/features/toolbox/actionTypes.js +++ b/react/features/toolbox/actionTypes.js @@ -7,6 +7,17 @@ */ export const CLEAR_TOOLBOX_TIMEOUT = Symbol('CLEAR_TOOLBOX_TIMEOUT'); +/** + * The type of (redux) action which updates whether the conference is or is not + * currently in full screen view. + * + * { + * type: FULL_SCREEN_CHANGED, + * fullScreen: boolean + * } + */ +export const FULL_SCREEN_CHANGED = Symbol('FULL_SCREEN_CHANGED'); + /** * The type of the action which sets the default toolbar buttons of the Toolbox. * @@ -19,6 +30,17 @@ export const CLEAR_TOOLBOX_TIMEOUT = Symbol('CLEAR_TOOLBOX_TIMEOUT'); export const SET_DEFAULT_TOOLBOX_BUTTONS = Symbol('SET_DEFAULT_TOOLBOX_BUTTONS'); +/** + * The type of (redux) action which requests full screen mode be entered or + * exited. + * + * { + * type: SET_FULL_SCREEN, + * fullScreen: boolean + * } + */ +export const SET_FULL_SCREEN = Symbol('SET_FULL_SCREEN'); + /** * The type of the action which sets the conference subject. * diff --git a/react/features/toolbox/actions.native.js b/react/features/toolbox/actions.native.js index 28a06a1fa..77d750a3f 100644 --- a/react/features/toolbox/actions.native.js +++ b/react/features/toolbox/actions.native.js @@ -229,9 +229,10 @@ export function toggleFullScreen(isFullScreen: boolean): Function { const buttonName = 'fullscreen'; const button = getButton(buttonName, getState()); - button.toggled = isFullScreen; - - dispatch(setToolbarButton(buttonName, button)); + if (button) { + button.toggled = isFullScreen; + dispatch(setToolbarButton(buttonName, button)); + } }; } diff --git a/react/features/toolbox/actions.web.js b/react/features/toolbox/actions.web.js index 8ff674c42..1ac139344 100644 --- a/react/features/toolbox/actions.web.js +++ b/react/features/toolbox/actions.web.js @@ -14,10 +14,13 @@ import { setToolboxTimeout, setToolboxTimeoutMS, setToolboxVisible, - toggleFullScreen, toggleToolbarButton } from './actions.native'; -import { SET_DEFAULT_TOOLBOX_BUTTONS } from './actionTypes'; +import { + FULL_SCREEN_CHANGED, + SET_DEFAULT_TOOLBOX_BUTTONS, + SET_FULL_SCREEN +} from './actionTypes'; import { getButton, getDefaultToolboxButtons, @@ -95,6 +98,23 @@ export function dockToolbox(dock: boolean): Function { }; } +/** + * Signals that full screen mode has been entered or exited. + * + * @param {boolean} fullScreen - Whether or not full screen mode is currently + * enabled. + * @returns {{ + * type: FULL_SCREEN_CHANGED, + * fullScreen: boolean + * }} + */ +export function fullScreenChanged(fullScreen: boolean) { + return { + type: FULL_SCREEN_CHANGED, + fullScreen + }; +} + /** * Returns button on mount/unmount handlers with dispatch function stored in * closure. @@ -106,8 +126,6 @@ export function dockToolbox(dock: boolean): Function { function _getButtonHandlers(dispatch) { const localRaiseHandHandler = (...args) => dispatch(changeLocalRaiseHand(...args)); - const toggleFullScreenHandler - = (...args) => dispatch(toggleFullScreen(...args)); return { /** @@ -119,22 +137,6 @@ function _getButtonHandlers(dispatch) { onMount: () => dispatch(showDesktopSharingButton()) }, - /** - * Mount/Unmount handler for toggling fullscreen button. - * - * @type {Object} - */ - fullscreen: { - onMount: () => - APP.UI.addListener( - UIEvents.FULLSCREEN_TOGGLED, - toggleFullScreenHandler), - onUnmount: () => - APP.UI.removeListener( - UIEvents.FULLSCREEN_TOGGLED, - toggleFullScreenHandler) - }, - /** * Mount/Unmount handlers for raisehand button. * @@ -291,6 +293,22 @@ export function showDialPadButton(show: boolean): Function { }; } +/** + * Signals a request to enter or exit full screen mode. + * + * @param {boolean} fullScreen - True to enter full screen mode, false to exit. + * @returns {{ + * type: SET_FULL_SCREEN, + * fullScreen: boolean + * }} + */ +export function setFullScreen(fullScreen: boolean) { + return { + type: SET_FULL_SCREEN, + fullScreen + }; +} + /** * Shows recording button. * diff --git a/react/features/toolbox/components/OverflowMenuButton.web.js b/react/features/toolbox/components/OverflowMenuButton.web.js new file mode 100644 index 000000000..fb332923f --- /dev/null +++ b/react/features/toolbox/components/OverflowMenuButton.web.js @@ -0,0 +1,106 @@ +import InlineDialog from '@atlaskit/inline-dialog'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +import { translate } from '../../base/i18n'; + +import ToolbarButtonV2 from './ToolbarButtonV2'; + +/** + * A React {@code Component} for opening or closing the {@code OverflowMenu}. + * + * @extends Component + */ +class OverflowMenuButton extends Component { + /** + * {@code OverflowMenuButton} component's property types. + * + * @static + */ + static propTypes = { + /** + * A child React Element to display within {@code InlineDialog}. + */ + children: PropTypes.object, + + /** + * Whether or not the OverflowMenu popover should display. + */ + isOpen: PropTypes.bool, + + /** + * Calback to change the visiblility of the overflow menu. + */ + onVisibilityChange: PropTypes.func, + + /** + * Invoked to obtain translated strings. + */ + t: PropTypes.func + }; + + /** + * Initializes a new {@code OverflowMenuButton} 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 per instance. + this._onCloseDialog = this._onCloseDialog.bind(this); + this._onToggleDialogVisibility + = this._onToggleDialogVisibility.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { children, isOpen, t } = this.props; + const iconClasses = `icon-thumb-menu ${isOpen ? 'toggled' : ''}`; + + return ( +
+ + + +
+ ); + } + + /** + * Callback invoked when {@code InlineDialog} signals that it should be + * close. + * + * @private + * @returns {void} + */ + _onCloseDialog() { + this.props.onVisibilityChange(false); + } + + /** + * Callback invoked to signal that an event has occurred that should change + * the visibility of the {@code InlineDialog} component. + * + * @private + * @returns {void} + */ + _onToggleDialogVisibility() { + this.props.onVisibilityChange(!this.props.isOpen); + } +} + +export default translate(OverflowMenuButton); diff --git a/react/features/toolbox/components/OverflowMenuItem.web.js b/react/features/toolbox/components/OverflowMenuItem.web.js new file mode 100644 index 000000000..ca91df0ce --- /dev/null +++ b/react/features/toolbox/components/OverflowMenuItem.web.js @@ -0,0 +1,53 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +/** + * A React {@code Component} for displaying a link to interact with other + * features of the application. + * + * @extends Component + */ +class OverflowMenuItem extends Component { + /** + * {@code OverflowMenuItem} component's property types. + * + * @static + */ + static propTypes = { + /** + * The icon class to use for displaying an icon before the link text. + */ + icon: PropTypes.string, + + /** + * The callback to invoke when {@code OverflowMenuItem} is clicked. + */ + onClick: PropTypes.func, + + /** + * The text to display in the {@code OverflowMenuItem}. + */ + text: PropTypes.string + }; + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + return ( +
  • + + + + { this.props.text } +
  • + ); + } +} + +export default OverflowMenuItem; diff --git a/react/features/toolbox/components/OverflowMenuProfileItem.native.js b/react/features/toolbox/components/OverflowMenuProfileItem.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/toolbox/components/OverflowMenuProfileItem.web.js b/react/features/toolbox/components/OverflowMenuProfileItem.web.js new file mode 100644 index 000000000..4a442eba5 --- /dev/null +++ b/react/features/toolbox/components/OverflowMenuProfileItem.web.js @@ -0,0 +1,119 @@ +/* globals interfaceConfig */ + +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { + Avatar, + getAvatarURL, + getLocalParticipant +} from '../../base/participants'; + +/** + * A React {@code Component} for displaying a link with a profile avatar as an + * icon. + * + * @extends Component + */ +class OverflowMenuProfileItem extends Component { + /** + * {@code OverflowMenuProfileItem}'s property types. + * + * @static + */ + static propTypes = { + /** + * The redux representation of the local participant. + */ + _localParticipant: PropTypes.object, + + /** + * Whether the button support clicking or not. + */ + _unclickable: PropTypes.bool, + + /** + * The callback to invoke when {@code OverflowMenuProfileItem} is + * clicked. + */ + onClick: PropTypes.func + }; + + /** + * Initializes a new {@code OverflowMenuProfileItem} instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props) { + super(props); + + // Bind event handler so it is only bound once for every instance. + this._onClick = this._onClick.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _localParticipant, _unclickable } = this.props; + const classNames = `overflow-menu-item ${ + _unclickable ? 'unclickable' : ''}`; + const avatarURL = getAvatarURL(_localParticipant); + let displayName; + + if (_localParticipant && _localParticipant.name) { + displayName = _localParticipant.name.split(' ')[0]; + } else { + displayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME; + } + + return ( +
  • + + + + + { displayName } + +
  • + ); + } + + /** + * Invokes an on click callback if clicking is allowed. + * + * @returns {void} + */ + _onClick() { + if (!this.props._unclickable) { + this.props.onClick(); + } + } +} + +/** + * Maps (parts of) the Redux state to the associated + * {@code OverflowMenuProfileItem} component's props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _localParticipant: Object, + * _unclickable: boolean + * }} + */ +function _mapStateToProps(state) { + return { + _localParticipant: getLocalParticipant(state), + _unclickable: !state['features/base/jwt'].isGuest + }; +} + +export default connect(_mapStateToProps)(OverflowMenuProfileItem); diff --git a/react/features/toolbox/components/ToolbarButtonV2.native.js b/react/features/toolbox/components/ToolbarButtonV2.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/toolbox/components/ToolbarButtonV2.web.js b/react/features/toolbox/components/ToolbarButtonV2.web.js new file mode 100644 index 000000000..348e0c0c3 --- /dev/null +++ b/react/features/toolbox/components/ToolbarButtonV2.web.js @@ -0,0 +1,78 @@ +import Tooltip from '@atlaskit/tooltip'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import AbstractToolbarButton from './AbstractToolbarButton'; + +/** + * Represents a button in the toolbar. + * + * @extends AbstractToolbarButton + */ +class ToolbarButtonV2 extends AbstractToolbarButton { + /** + * Default values for {@code ToolbarButtonV2} component's properties. + * + * @static + */ + static defaultProps = { + tooltipPosition: 'top' + }; + + /** + * {@code ToolbarButtonV2} component's property types. + * + * @static + */ + static propTypes = { + ...AbstractToolbarButton.propTypes, + + /** + * The text to display in the tooltip. + */ + tooltip: PropTypes.string, + + /** + * From which direction the tooltip should appear, relative to the + * button. + */ + tooltipPosition: PropTypes.string + } + + /** + * Renders the button of this {@code ToolbarButton}. + * + * @param {Object} children - The children, if any, to be rendered inside + * the button. Presumably, contains the icon of this {@code ToolbarButton}. + * @protected + * @returns {ReactElement} The button of this {@code ToolbarButton}. + */ + _renderButton(children) { + return ( +
    + + { children } + +
    + ); + } + + /** + * Renders the icon of this {@code ToolbarButton}. + * + * @inheritdoc + */ + _renderIcon() { + return ( +
    + +
    + ); + } +} + +export default ToolbarButtonV2; diff --git a/react/features/toolbox/components/Toolbox.native.js b/react/features/toolbox/components/Toolbox.native.js index 8d9e6aaf6..44a03a57b 100644 --- a/react/features/toolbox/components/Toolbox.native.js +++ b/react/features/toolbox/components/Toolbox.native.js @@ -4,26 +4,16 @@ import React, { Component } from 'react'; import { View } from 'react-native'; import { connect } from 'react-redux'; -import { - AUDIO_MUTE, - VIDEO_MUTE, - createToolbarEvent, - sendAnalytics -} from '../../analytics'; import { toggleAudioOnly } from '../../base/conference'; import { MEDIA_TYPE, - setAudioMuted, - setVideoMuted, - toggleCameraFacingMode, - VIDEO_MUTISM_AUTHORITY + toggleCameraFacingMode } from '../../base/media'; import { Container } from '../../base/react'; import { isNarrowAspectRatio, makeAspectRatioAware } from '../../base/responsive-ui'; -import { ColorPalette } from '../../base/styles'; import { EnterPictureInPictureToolbarButton } from '../../mobile/picture-in-picture'; @@ -39,6 +29,8 @@ import AudioRouteButton from './AudioRouteButton'; import styles from './styles'; import ToolbarButton from './ToolbarButton'; +import { AudioMuteButton, HangupButton, VideoMuteButton } from './buttons'; + /** * The indicator which determines (at bundle time) whether there should be a * {@code ToolbarButton} in {@code Toolbox} to expose the functionality of the @@ -118,20 +110,6 @@ type Props = { * Implements the conference toolbox on React Native. */ class Toolbox extends Component { - /** - * Initializes a new {@code Toolbox} instance. - * - * @param {Props} props - The read-only React {@code Component} props with - * which the new instance is to be initialized. - */ - constructor(props: Props) { - super(props); - - // Bind event handlers so they are only bound once per instance. - this._onToggleAudio = this._onToggleAudio.bind(this); - this._onToggleVideo = this._onToggleVideo.bind(this); - } - /** * Implements React's {@link Component#render()}. * @@ -194,64 +172,6 @@ class Toolbox extends Component { }; } - _onToggleAudio: () => void; - - /** - * Dispatches an action to toggle the mute state of the audio/microphone. - * - * @private - * @returns {void} - */ - _onToggleAudio() { - const mute = !this.props._audioMuted; - - sendAnalytics(createToolbarEvent( - AUDIO_MUTE, - { - enable: mute - })); - - // The user sees the reality i.e. the state of base/tracks and intends - // to change reality by tapping on the respective button i.e. the user - // sets the state of base/media. Whether the user's intention will turn - // into reality is a whole different story which is of no concern to the - // tapping. - this.props.dispatch( - setAudioMuted( - mute, - VIDEO_MUTISM_AUTHORITY.USER, - /* ensureTrack */ true)); - } - - _onToggleVideo: () => void; - - /** - * Dispatches an action to toggle the mute state of the video/camera. - * - * @private - * @returns {void} - */ - _onToggleVideo() { - const mute = !this.props._videoMuted; - - sendAnalytics(createToolbarEvent( - VIDEO_MUTE, - { - enable: mute - })); - - // The user sees the reality i.e. the state of base/tracks and intends - // to change reality by tapping on the respective button i.e. the user - // sets the state of base/media. Whether the user's intention will turn - // into reality is a whole different story which is of no concern to the - // tapping. - this.props.dispatch( - setVideoMuted( - !this.props._videoMuted, - VIDEO_MUTISM_AUTHORITY.USER, - /* ensureTrack */ true)); - } - /** * Renders the toolbar which contains the primary buttons such as hangup, * audio and video mute. @@ -269,24 +189,9 @@ class Toolbox extends Component { - - - + + + ); diff --git a/react/features/toolbox/components/Toolbox.web.js b/react/features/toolbox/components/Toolbox.web.js index 0f5f4b63a..6dfa99e95 100644 --- a/react/features/toolbox/components/Toolbox.web.js +++ b/react/features/toolbox/components/Toolbox.web.js @@ -5,8 +5,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { - setDefaultToolboxButtons, - setToolboxAlwaysVisible + setDefaultToolboxButtons } from '../actions'; import { abstractMapStateToProps @@ -39,11 +38,6 @@ class Toolbox extends Component<*> { */ _setDefaultToolboxButtons: PropTypes.func, - /** - * Handler dispatching reset always visible toolbox action. - */ - _setToolboxAlwaysVisible: PropTypes.func, - /** * Represents conference subject. */ @@ -67,8 +61,6 @@ class Toolbox extends Component<*> { * @returns {void} */ componentDidMount(): void { - this.props._setToolboxAlwaysVisible(); - // FIXME The redux action SET_DEFAULT_TOOLBOX_BUTTONS and related source // code such as the redux action creator setDefaultToolboxButtons and // _setDefaultToolboxButtons were introduced to solve the following bug @@ -168,8 +160,7 @@ class Toolbox extends Component<*> { * * @param {Function} dispatch - Redux action dispatcher. * @returns {{ - * _setDefaultToolboxButtons: Function, - * _setToolboxAlwaysVisible: Function + * _setDefaultToolboxButtons: Function * }} * @private */ @@ -182,18 +173,6 @@ function _mapDispatchToProps(dispatch: Function): Object { */ _setDefaultToolboxButtons() { dispatch(setDefaultToolboxButtons()); - }, - - /** - * Dispatches a (redux) action to reset the permanent visibility of - * the Toolbox. - * - * @returns {Object} Dispatched action. - */ - _setToolboxAlwaysVisible() { - dispatch(setToolboxAlwaysVisible( - config.alwaysVisibleToolbar === true - || interfaceConfig.filmStripOnly)); } }; } diff --git a/react/features/toolbox/components/ToolboxFilmstrip.native.js b/react/features/toolbox/components/ToolboxFilmstrip.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/toolbox/components/ToolboxFilmstrip.web.js b/react/features/toolbox/components/ToolboxFilmstrip.web.js new file mode 100644 index 000000000..d4fb84d9c --- /dev/null +++ b/react/features/toolbox/components/ToolboxFilmstrip.web.js @@ -0,0 +1,95 @@ +// @flow + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { createToolbarEvent, sendAnalytics } from '../../analytics'; +import { translate } from '../../base/i18n'; +import { openDeviceSelectionDialog } from '../../device-selection'; + +import ToolbarButtonV2 from './ToolbarButtonV2'; +import { AudioMuteButton, HangupButton, VideoMuteButton } from './buttons'; + +declare var interfaceConfig: Object; + +/** + * Implements the conference toolbox on React/Web for filmstrip only mode. + * + * @extends Component + */ +class ToolboxFilmstrip extends Component<*> { + _visibleButtons: Object; + + /** + * Initializes a new {@code ToolboxFilmstrip} instance. + * + * @param {Props} props - The read-only React {@code Component} props with + * which the new instance is to be initialized. + */ + constructor(props) { + super(props); + + this._visibleButtons = new Set(interfaceConfig.TOOLBAR_BUTTONS); + + // Bind event handlers so they are only bound once per instance. + this._onToolbarOpenSettings = this._onToolbarOpenSettings.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { t } = this.props; + + return ( +
    + { this._shouldShowButton('microphone') + && } + { this._shouldShowButton('camera') + && } + { this._shouldShowButton('fodeviceselection') + && } + { this._shouldShowButton('hangup') + && } +
    + ); + } + + _onToolbarOpenSettings: () => void; + + /** + * Creates an analytics toolbar event for and dispatches an action to open + * the device selection popup dialog. + * + * @private + * @returns {void} + */ + _onToolbarOpenSettings() { + sendAnalytics(createToolbarEvent('filmstrip.only.device.selection')); + + this.props.dispatch(openDeviceSelectionDialog()); + } + + _shouldShowButton: (string) => boolean; + + /** + * Returns if a button name has been explicitly configured to be displayed. + * + * @param {string} buttonName - The name of the button, as expected in + * {@link intefaceConfig}. + * @private + * @returns {boolean} True if the button should be displayed. + */ + _shouldShowButton(buttonName) { + return this._visibleButtons.has(buttonName); + } +} + +export default translate(connect()(ToolboxFilmstrip)); diff --git a/react/features/toolbox/components/ToolboxV2.native.js b/react/features/toolbox/components/ToolboxV2.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/toolbox/components/ToolboxV2.web.js b/react/features/toolbox/components/ToolboxV2.web.js new file mode 100644 index 000000000..3f1eb5f60 --- /dev/null +++ b/react/features/toolbox/components/ToolboxV2.web.js @@ -0,0 +1,1116 @@ +// @flow + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { + ACTION_SHORTCUT_TRIGGERED, + createShortcutEvent, + createToolbarEvent, + sendAnalytics +} from '../../analytics'; +import { openDialog } from '../../base/dialog'; +import { translate } from '../../base/i18n'; +import { + PARTICIPANT_ROLE, + getLocalParticipant, + participantUpdated +} from '../../base/participants'; +import { getLocalVideoTrack, toggleScreensharing } from '../../base/tracks'; +import { ChatCounter } from '../../chat'; +import { openDeviceSelectionDialog } from '../../device-selection'; +import { toggleDocument } from '../../etherpad'; +import { openFeedbackDialog } from '../../feedback'; +import { AddPeopleDialog, InfoDialogButton } from '../../invite'; +import { openKeyboardShortcutsDialog } from '../../keyboard-shortcuts'; +import { RECORDING_TYPES, toggleRecording } from '../../recording'; +import { toggleSharedVideo } from '../../shared-video'; +import { toggleChat, toggleProfile, toggleSettings } from '../../side-panel'; +import { SpeakerStats } from '../../speaker-stats'; +import { VideoQualityDialog } from '../../video-quality'; + +import { setFullScreen, setToolbarHovered } from '../actions'; + +import OverflowMenuButton from './OverflowMenuButton'; +import OverflowMenuItem from './OverflowMenuItem'; +import OverflowMenuProfileItem from './OverflowMenuProfileItem'; +import ToolbarButtonV2 from './ToolbarButtonV2'; +import { AudioMuteButton, HangupButton, VideoMuteButton } from './buttons'; + +type Props = { + + /** + * Whether or not the feature for adding people directly into the call + * is enabled. + */ + _addPeopleAvailable: boolean, + + /** + * Whether or not the chat feature is currently displayed. + */ + _chatOpen: boolean, + + /** + * The {@code JitsiConference} for the current conference. + */ + _conference: Object, + + /** + * Whether or not screensharing is initialized. + */ + _desktopSharingEnabled: boolean, + + /** + * Whether or not the feature for telephony to dial out to a number is + * enabled. + */ + _dialOutAvailable: boolean, + + /** + * Whether or not a dialog is displayed. + */ + _dialog: boolean, + + /** + * Whether or not the local participant is currently editing a document. + */ + _editingDocument: boolean, + + /** + * Whether or not collaborative document editing is enabled. + */ + _etherpadInitialized: boolean, + + /** + * Whether or not call feedback can be sent. + */ + _feedbackConfigured: boolean, + + /** + * Whether or not the app is currently in full screen. + */ + _fullScreen: boolean, + + /** + * Whether or not the conference is currently being recorded by the local + * participant. + */ + _isRecording: boolean, + + /** + * The ID of the local participant. + */ + _localParticipantID: String, + + /** + * Whether or not the local participant's hand is raised. + */ + _raisedHand: boolean, + + /** + * Whether or not the recording feature is enabled for use. + */ + _recordingEnabled: boolean, + + /** + * Whether the recording feature is live streaming (jibri) or is file + * recording (jirecon). + */ + _recordingType: String, + + /** + * Whether or not the local participant is screensharing. + */ + _screensharing: boolean, + + /** + * Whether or not the local participant is sharing a YouTube video. + */ + _sharingVideo: boolean, + + /** + * Flag showing whether toolbar is visible. + */ + _visible: boolean, + + /** + * Invoked to active other features of the app. + */ + dispatch: Function, + + /** + * Invoked to obtain translated strings. + */ + t: Function +} + +type State = { + + /** + * Whether or not the overflow menu is visible. + */ + showOverflowMenu: boolean +} + +declare var APP: Object; +declare var interfaceConfig: Object; + +/** + * Implements the conference toolbox on React/Web. + * + * @extends Component + */ +class ToolboxV2 extends Component { + _visibleButtons: Object; + + state = { + showOverflowMenu: false + } + + /** + * Initializes a new {@code Toolbox} instance. + * + * @param {Props} props - The read-only React {@code Component} props with + * which the new instance is to be initialized. + */ + constructor(props: Props) { + super(props); + + this._visibleButtons = new Set(interfaceConfig.TOOLBAR_BUTTONS); + + // Bind event handlers so they are only bound once per instance. + this._onMouseOut = this._onMouseOut.bind(this); + this._onMouseOver = this._onMouseOver.bind(this); + this._onSetOverflowVisible = this._onSetOverflowVisible.bind(this); + + this._onShortcutToggleChat = this._onShortcutToggleChat.bind(this); + this._onShortcutToggleFullScreen + = this._onShortcutToggleFullScreen.bind(this); + this._onShortcutToggleRaiseHand + = this._onShortcutToggleRaiseHand.bind(this); + this._onShortcutToggleScreenshare + = this._onShortcutToggleScreenshare.bind(this); + + this._onToolbarOpenFeedback + = this._onToolbarOpenFeedback.bind(this); + this._onToolbarOpenInvite = this._onToolbarOpenInvite.bind(this); + this._onToolbarOpenKeyboardShortcuts + = this._onToolbarOpenKeyboardShortcuts.bind(this); + this._onToolbarOpenSpeakerStats + = this._onToolbarOpenSpeakerStats.bind(this); + this._onToolbarOpenVideoQuality + = this._onToolbarOpenVideoQuality.bind(this); + + this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this); + this._onToolbarToggleEtherpad + = this._onToolbarToggleEtherpad.bind(this); + this._onToolbarToggleFullScreen + = this._onToolbarToggleFullScreen.bind(this); + this._onToolbarToggleProfile + = this._onToolbarToggleProfile.bind(this); + this._onToolbarToggleRaiseHand + = this._onToolbarToggleRaiseHand.bind(this); + this._onToolbarToggleRecording + = this._onToolbarToggleRecording.bind(this); + this._onToolbarToggleScreenshare + = this._onToolbarToggleScreenshare.bind(this); + this._onToolbarToggleSettings + = this._onToolbarToggleSettings.bind(this); + this._onToolbarToggleSharedVideo + = this._onToolbarToggleSharedVideo.bind(this); + } + + /** + * Sets keyboard shortcuts for to trigger ToolbarButtons actions. + * + * @inheritdoc + * @returns {void} + */ + componentDidMount() { + const KEYBOARD_SHORTCUTS = [ + this._shouldShowButton('chat') && { + character: 'C', + exec: this._onShortcutToggleChat, + helpDescription: 'keyboardShortcuts.toggleChat' + }, + this._shouldShowButton('desktop') && { + character: 'D', + exec: this._onShortcutToggleScreenshare, + helpDescription: 'keyboardShortcuts.toggleScreensharing' + }, + this._shouldShowButton('raisehand') && { + character: 'R', + exec: this._onShortcutToggleRaiseHand, + helpDescription: 'keyboardShortcuts.raiseHand' + }, + this._shouldShowButton('fullscreen') && { + character: 'S', + exec: this._onShortcutToggleFullScreen, + helpDescription: 'keyboardShortcuts.fullScreen' + } + ]; + + KEYBOARD_SHORTCUTS.forEach(shortcut => { + if (typeof shortcut === 'object') { + APP.keyboardshortcut.registerShortcut( + shortcut.character, + null, + shortcut.exec, + shortcut.helpDescription); + } + }); + } + + /** + * Update the visibility of the {@code OverflowMenuButton}. + * + * @inheritdoc + */ + componentWillReceiveProps(nextProps) { + // Ensure the dialog is closed when the toolbox becomes hidden. + if (this.state.showOverflowMenu && !nextProps._visible) { + this._onSetOverflowVisible(false); + } + + if (this.state.showOverflowMenu + && !this.props._dialog + && nextProps._dialog) { + this._onSetOverflowVisible(false); + this.props.dispatch(setToolbarHovered(false)); + } + } + + /** + * Removes keyboard shortcuts registered by this component. + * + * @inheritdoc + * @returns {void} + */ + componentWillUnmount() { + [ 'C', 'D', 'R', 'S' ].forEach(letter => + APP.keyboardshortcut.unregisterShortcut(letter)); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { + _addPeopleAvailable, + _chatOpen, + _dialOutAvailable, + _raisedHand, + _visible, + t + } = this.props; + const rootClassNames = `new-toolbox ${_visible ? 'visible' : ''} ${ + this._visibleButtons.size ? '' : 'no-buttons'}`; + const overflowMenuContent = this._renderOverflowMenuContent(); + const overflowHasItems = Boolean(overflowMenuContent.filter( + child => child).length); + + return ( +
    +
    + { this._shouldShowButton('desktop') + && this._renderDesktopSharingButton() } + { this._shouldShowButton('raisehand') + && } + { this._shouldShowButton('chat') + &&
    + + +
    } +
    +
    + { this._shouldShowButton('microphone') + && } + { this._shouldShowButton('hangup') + && } + { this._shouldShowButton('camera') + && } +
    +
    + { this._shouldShowButton('invite') + && } + { this._shouldShowButton('info') && } + { overflowHasItems + && +
      + { overflowMenuContent } +
    +
    } +
    +
    + ); + } + + /** + * Callback invoked to display {@code FeedbackDialog}. + * + * @private + * @returns {void} + */ + _doOpenFeedback() { + this.props.dispatch(openFeedbackDialog()); + } + + /** + * Opens the dialog for inviting people directly into the conference. + * + * @private + * @returns {void} + */ + _doOpenInvite() { + const { _addPeopleAvailable, _dialOutAvailable, dispatch } = this.props; + + if (_addPeopleAvailable || _dialOutAvailable) { + dispatch(openDialog(AddPeopleDialog, { + enableAddPeople: _addPeopleAvailable, + enableDialOut: _dialOutAvailable + })); + } + } + + /** + * Dispatches an action to display {@code KeyboardShortcuts}. + * + * @private + * @returns {void} + */ + _doOpenKeyboardShorcuts() { + this.props.dispatch(openKeyboardShortcutsDialog()); + } + + /** + * Callback invoked to display {@code SpeakerStats}. + * + * @private + * @returns {void} + */ + _doOpenSpeakerStats() { + this.props.dispatch(openDialog(SpeakerStats, { + conference: this.props._conference + })); + } + + /** + * Dispatches an action to toggle the video quality dialog. + * + * @private + * @returns {void} + */ + _doOpenVideoQuality() { + this.props.dispatch(openDialog(VideoQualityDialog)); + } + + /** + * Dispatches an action to toggle the display of chat. + * + * @private + * @returns {void} + */ + _doToggleChat() { + this.props.dispatch(toggleChat()); + } + + /** + * Dispatches an action to show or hide document editing. + * + * @private + * @returns {void} + */ + _doToggleEtherpad() { + this.props.dispatch(toggleDocument()); + } + + /** + * Dispatches an action to toggle screensharing. + * + * @private + * @returns {void} + */ + _doToggleFullScreen() { + const fullScreen = !this.props._fullScreen; + + this.props.dispatch(setFullScreen(fullScreen)); + } + + /** + * Dispatches an action to show or hide the profile edit panel. + * + * @private + * @returns {void} + */ + _doToggleProfile() { + this.props.dispatch(toggleProfile()); + } + + /** + * Dispatches an action to toggle the local participant's raised hand state. + * + * @private + * @returns {void} + */ + _doToggleRaiseHand() { + const { _localParticipantID, _raisedHand } = this.props; + + this.props.dispatch(participantUpdated({ + id: _localParticipantID, + local: true, + raisedHand: !_raisedHand + })); + } + + /** + * Dispatches an action to toggle recording. + * + * @private + * @returns {void} + */ + _doToggleRecording() { + this.props.dispatch(toggleRecording()); + } + + /** + * Dispatches an action to toggle screensharing. + * + * @private + * @returns {void} + */ + _doToggleScreenshare() { + if (this.props._desktopSharingEnabled) { + this.props.dispatch(toggleScreensharing()); + } + } + + /** + * Dispatches an action to toggle display of settings, be it the settings + * panel or directly to device selection. + * + * @private + * @returns {void} + */ + _doToggleSettings() { + if (interfaceConfig.SETTINGS_SECTIONS.length === 1 + && interfaceConfig.SETTINGS_SECTIONS.includes('devices')) { + this.props.dispatch(openDeviceSelectionDialog()); + } else { + this.props.dispatch(toggleSettings()); + } + } + + /** + * Dispatches an action to toggle YouTube video sharing. + * + * @private + * @returns {void} + */ + _doToggleSharedVideo() { + this.props.dispatch(toggleSharedVideo()); + } + + _onMouseOut: () => void; + + /** + * Dispatches an action signaling the toolbar is not being hovered. + * + * @private + * @returns {void} + */ + _onMouseOut() { + this.props.dispatch(setToolbarHovered(false)); + } + + _onMouseOver: () => void; + + /** + * Dispatches an action signaling the toolbar is being hovered. + * + * @private + * @returns {void} + */ + _onMouseOver() { + this.props.dispatch(setToolbarHovered(true)); + } + + _onSetOverflowVisible: (boolean) => void; + + /** + * Sets the visibility of the overflow menu. + * + * @param {boolean} visible - Whether or not the overflow menu should be + * displayed. + * @private + * @returns {void} + */ + _onSetOverflowVisible(visible) { + this.setState({ showOverflowMenu: visible }); + } + + _onShortcutToggleChat: () => void; + + /** + * Creates an analytics keyboard shortcut event and dispatches an action for + * toggling the display of chat. + * + * @private + * @returns {void} + */ + _onShortcutToggleChat() { + sendAnalytics(createShortcutEvent( + 'toggle.chat', + { + enable: !this.props._chatOpen + })); + + this._doToggleChat(); + } + + _onShortcutToggleFullScreen: () => void; + + /** + * Creates an analytics keyboard shortcut event and dispatches an action for + * toggling full screen mode. + * + * @private + * @returns {void} + */ + _onShortcutToggleFullScreen() { + sendAnalytics(createShortcutEvent( + 'toggle.fullscreen', + { + enable: !this.props._fullScreen + })); + + this._doToggleFullScreen(); + } + + _onShortcutToggleRaiseHand: () => void; + + /** + * Creates an analytics keyboard shortcut event and dispatches an action for + * toggling raise hand. + * + * @private + * @returns {void} + */ + _onShortcutToggleRaiseHand() { + sendAnalytics(createShortcutEvent( + 'toggle.raise.hand', + ACTION_SHORTCUT_TRIGGERED, + { enable: !this.props._raisedHand })); + + this._doToggleRaiseHand(); + } + + _onShortcutToggleScreenshare: () => void; + + /** + * Creates an analytics keyboard shortcut event and dispatches an action for + * toggling screensharing. + * + * @private + * @returns {void} + */ + _onShortcutToggleScreenshare() { + sendAnalytics(createToolbarEvent( + 'screen.sharing', + { + enable: !this.props._screensharing + })); + + this._doToggleScreenshare(); + } + + _onToolbarOpenFeedback: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * display of feedback. + * + * @private + * @returns {void} + */ + _onToolbarOpenFeedback() { + sendAnalytics(createToolbarEvent('feedback')); + + this._doOpenFeedback(); + } + + _onToolbarOpenInvite: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for opening + * the modal for inviting people directly into the conference. + * + * @private + * @returns {void} + */ + _onToolbarOpenInvite() { + sendAnalytics(createToolbarEvent('invite')); + + this._doOpenInvite(); + } + + _onToolbarOpenKeyboardShortcuts: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for opening + * the modal for showing available keyboard shortcuts. + * + * @private + * @returns {void} + */ + _onToolbarOpenKeyboardShortcuts() { + sendAnalytics(createToolbarEvent('shortcuts')); + + this._doOpenKeyboardShorcuts(); + } + + _onToolbarOpenSpeakerStats: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for opening + * the speaker stats modal. + * + * @private + * @returns {void} + */ + _onToolbarOpenSpeakerStats() { + sendAnalytics(createToolbarEvent('speaker.stats')); + + this._doOpenSpeakerStats(); + } + + _onToolbarOpenVideoQuality: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * open the video quality dialog. + * + * @private + * @returns {void} + */ + _onToolbarOpenVideoQuality() { + sendAnalytics(createToolbarEvent('video.quality')); + + this._doOpenVideoQuality(); + } + + _onToolbarToggleChat: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * the display of chat. + * + * @private + * @returns {void} + */ + _onToolbarToggleChat() { + sendAnalytics(createToolbarEvent( + 'toggle.chat', + { + enable: !this.props._chatOpen + })); + + this._doToggleChat(); + } + + _onToolbarToggleEtherpad: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * the display of document editing. + * + * @private + * @returns {void} + */ + _onToolbarToggleEtherpad() { + sendAnalytics(createToolbarEvent( + 'toggle.etherpad', + { + enable: !this.props._editingDocument + })); + + this._doToggleEtherpad(); + } + + _onToolbarToggleFullScreen: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * full screen mode. + * + * @private + * @returns {void} + */ + _onToolbarToggleFullScreen() { + sendAnalytics(createToolbarEvent( + 'toggle.fullscreen', + { + enable: !this.props._fullScreen + })); + + this._doToggleFullScreen(); + } + + _onToolbarToggleOverflowMenu: () => void; + + /** + * Callback invoked to change whether the {@code OverflowMenu} is displayed + * or not. + * + * @private + * @returns {void} + */ + _onToolbarToggleOverflowMenu() { + sendAnalytics(createToolbarEvent('overflow')); + + this.setState({ showOverflowMenu: !this.state.showOverflowMenu }); + } + + _onToolbarToggleProfile: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for showing + * or hiding the profile edit panel. + * + * @private + * @returns {void} + */ + _onToolbarToggleProfile() { + sendAnalytics(createToolbarEvent('profile')); + + this._doToggleProfile(); + } + + _onToolbarToggleRaiseHand: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * raise hand. + * + * @private + * @returns {void} + */ + _onToolbarToggleRaiseHand() { + sendAnalytics(createToolbarEvent( + 'raise.hand', + { enable: !this.props._raisedHand })); + + this._doToggleRaiseHand(); + } + + _onToolbarToggleRecording: () => void; + + /** + * Dispatches an action to toggle recording. + * + * @private + * @returns {void} + */ + _onToolbarToggleRecording() { + // No analytics handling is added here for the click as this action will + // exercise the old toolbar UI flow, which includes analytics handling. + + this._doToggleRecording(); + } + + _onToolbarToggleScreenshare: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * screensharing. + * + * @private + * @returns {void} + */ + _onToolbarToggleScreenshare() { + sendAnalytics(createShortcutEvent( + 'toggle.screen.sharing', + ACTION_SHORTCUT_TRIGGERED, + { enable: !this.props._screensharing })); + + this._doToggleScreenshare(); + } + + _onToolbarToggleSettings: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * settings display. + * + * @private + * @returns {void} + */ + _onToolbarToggleSettings() { + sendAnalytics(createToolbarEvent('settings')); + + this._doToggleSettings(); + } + + _onToolbarToggleSharedVideo: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * the sharing of a YouTube video. + * + * @private + * @returns {void} + */ + _onToolbarToggleSharedVideo() { + sendAnalytics(createToolbarEvent('shared.video.toggled', + { + enable: !this.props._sharingVideo + })); + + this._doToggleSharedVideo(); + } + + /** + * Renders a button for togglein screen sharing. + * + * @private + * @returns {ReactElement} + */ + _renderDesktopSharingButton() { + const { _desktopSharingEnabled, _screensharing, t } = this.props; + const classNames = `icon-share-desktop ${ + _screensharing ? 'toggled' : ''} ${ + _desktopSharingEnabled ? '' : 'disabled'}`; + const tooltip = _desktopSharingEnabled + ? t('toolbar.sharescreen') + : interfaceConfig.DESKTOP_SHARING_BUTTON_DISABLED_TOOLTIP + || t('toolbar.sharescreenDisabled'); + + return ( + + ); + } + + /** + * Renders the list elements of the overflow menu. + * + * @private + * @returns {Array} + */ + _renderOverflowMenuContent() { + const { + _editingDocument, + _etherpadInitialized, + _feedbackConfigured, + _fullScreen, + _sharingVideo, + t + } = this.props; + + return [ + this._shouldShowButton('profile') + && , + this._shouldShowButton('settings') + && , + this._shouldShowButton('sharedvideo') + && , + this._shouldShowButton('etherpad') + && _etherpadInitialized + && , + this._shouldShowButton('fullscreen') + && , + this._renderRecordingButton(), + this._shouldShowButton('videoquality') + && , + this._shouldShowButton('stats') + && , + this._shouldShowButton('feedback') + && _feedbackConfigured + && , + this._shouldShowButton('shortcuts') + && + ]; + } + + /** + * Renders an {@code OverflowMenuItem} depending on the current recording + * state. + * + * @private + * @returns {ReactElement|null} + */ + _renderRecordingButton() { + const { + _isRecording, + _recordingEnabled, + _recordingType, + t + } = this.props; + + if (!_recordingEnabled || !this._shouldShowButton('recording')) { + return null; + } + + let translationKey; + + if (_recordingType === RECORDING_TYPES.JIBRI) { + translationKey = _isRecording + ? 'dialog.stopLiveStreaming' + : 'dialog.startLiveStreaming'; + } else { + translationKey = _isRecording + ? 'dialog.stopRecording' + : 'dialog.startRecording'; + } + + return ( + + ); + } + + _shouldShowButton: (string) => boolean; + + /** + * Returns if a button name has been explicitly configured to be displayed. + * + * @param {string} buttonName - The name of the button, as expected in + * {@link intefaceConfig}. + * @private + * @returns {boolean} True if the button should be displayed. + */ + _shouldShowButton(buttonName) { + return this._visibleButtons.has(buttonName); + } +} + +/** + * Maps (parts of) the redux state to {@link Toolbox}'s React {@code Component} + * props. + * + * @param {Object} state - The redux store/state. + * @private + * @returns {{}} + */ +function _mapStateToProps(state) { + const { + conference, + desktopSharingEnabled + } = state['features/base/conference']; + const { + callStatsID, + enableRecording, + enableUserRolesBasedOnToken + } = state['features/base/config']; + const { isGuest } = state['features/base/jwt']; + const { isRecording, recordingType } = state['features/recording']; + const sharedVideoStatus = state['features/shared-video'].status; + const { current } = state['features/side-panel']; + const { + alwaysVisible, + fullScreen, + timeoutID, + visible + } = state['features/toolbox']; + const localParticipant = getLocalParticipant(state); + const localVideo = getLocalVideoTrack(state['features/base/tracks']); + const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR; + + return { + _addPeopleAvailable: !isGuest, + _chatOpen: current === 'chat_container', + _conference: conference, + _desktopSharingEnabled: desktopSharingEnabled, + _dialOutAvailable: isModerator + && conference && conference.isSIPCallingSupported() + && (!enableUserRolesBasedOnToken || !isGuest), + _dialog: Boolean(state['features/base/dialog'].component), + _editingDocument: Boolean(state['features/etherpad'].editing), + _etherpadInitialized: Boolean(state['features/etherpad'].initialized), + _feedbackConfigured: Boolean(callStatsID), + _isRecording: isRecording, + _fullScreen: fullScreen, + _localParticipantID: localParticipant.id, + _raisedHand: localParticipant.raisedHand, + _recordingEnabled: isModerator && enableRecording + && (conference && conference.isRecordingSupported()), + _recordingType: recordingType, + _screensharing: localVideo && localVideo.videoType === 'desktop', + _sharingVideo: sharedVideoStatus === 'playing' + || sharedVideoStatus === 'start' + || sharedVideoStatus === 'pause', + _visible: Boolean(timeoutID || visible || alwaysVisible) + }; +} + +export default translate(connect(_mapStateToProps)(ToolboxV2)); diff --git a/react/features/toolbox/components/buttons/AbstractAudioMuteButton.js b/react/features/toolbox/components/buttons/AbstractAudioMuteButton.js new file mode 100644 index 000000000..c86e9a127 --- /dev/null +++ b/react/features/toolbox/components/buttons/AbstractAudioMuteButton.js @@ -0,0 +1,87 @@ +// @flow + +import PropTypes from 'prop-types'; +import { Component } from 'react'; + +import { + AUDIO_MUTE, + createToolbarEvent, + sendAnalytics +} from '../../../analytics'; +import { + VIDEO_MUTISM_AUTHORITY, + setAudioMuted +} from '../../../base/media'; + +/** + * An abstract implementation of a button for toggling audio mute. + */ +export default class AbstractAudioMuteButton extends Component<*> { + /** + * {@code AbstractAudioMuteButton} component's property types. + * + * @static + */ + static propTypes = { + /** + * Whether or not the local microphone is muted. + */ + _audioMuted: PropTypes.bool, + + /** + * Invoked to toggle audio mute. + */ + dispatch: PropTypes.func + }; + + /** + * Initializes a new {@code AbstractAudioMuteButton} instance. + * + * @param {Props} props - The read-only React {@code Component} props with + * which the new instance is to be initialized. + */ + constructor(props: Object) { + super(props); + + // Bind event handler so it is only bound once per instance. + this._onToolbarToggleAudio = this._onToolbarToggleAudio.bind(this); + } + + /** + * Dispatches an action to toggle audio mute. + * + * @private + * @returns {void} + */ + _doToggleAudio() { + // The user sees the reality i.e. the state of base/tracks and intends + // to change reality by tapping on the respective button i.e. the user + // sets the state of base/media. Whether the user's intention will turn + // into reality is a whole different story which is of no concern to the + // tapping. + this.props.dispatch( + setAudioMuted( + !this.props._audioMuted, + VIDEO_MUTISM_AUTHORITY.USER, + /* ensureTrack */ true)); + } + + _onToolbarToggleAudio: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * audio mute. + * + * @private + * @returns {void} + */ + _onToolbarToggleAudio() { + sendAnalytics(createToolbarEvent( + AUDIO_MUTE, + { + enable: !this.props._audioMuted + })); + + this._doToggleAudio(); + } +} diff --git a/react/features/toolbox/components/buttons/AbstractHangupButton.js b/react/features/toolbox/components/buttons/AbstractHangupButton.js new file mode 100644 index 000000000..0f6d765ff --- /dev/null +++ b/react/features/toolbox/components/buttons/AbstractHangupButton.js @@ -0,0 +1,51 @@ +// @flow + +import { Component } from 'react'; + +import { + createToolbarEvent, + sendAnalytics +} from '../../../analytics'; + +/** + * An abstract implementation of a button for leaving the conference. + */ +export default class AbstractHangupButton extends Component<*> { + /** + * Initializes a new {@code AbstractHangupButton} instance. + * + * @param {Props} props - The read-only React {@code Component} props with + * which the new instance is to be initialized. + */ + constructor(props: Object) { + super(props); + + // Bind event handler so it is only bound once per instance. + this._onToolbarHangup = this._onToolbarHangup.bind(this); + } + + /** + * Dispatches an action for leaving the current conference. + * + * @private + * @returns {void} + */ + _doHangup() { + /* to be implemented by descendants */ + } + + _onToolbarHangup: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for leaving + * the current conference. + * + * @private + * @returns {void} + */ + _onToolbarHangup() { + sendAnalytics(createToolbarEvent('hangup')); + + this._doHangup(); + } +} diff --git a/react/features/toolbox/components/buttons/AbstractVideoMuteButton.js b/react/features/toolbox/components/buttons/AbstractVideoMuteButton.js new file mode 100644 index 000000000..43f0230f3 --- /dev/null +++ b/react/features/toolbox/components/buttons/AbstractVideoMuteButton.js @@ -0,0 +1,88 @@ +// @flow + +import PropTypes from 'prop-types'; +import { Component } from 'react'; + +import { + VIDEO_MUTE, + createToolbarEvent, + sendAnalytics +} from '../../../analytics'; +import { + VIDEO_MUTISM_AUTHORITY, + setVideoMuted +} from '../../../base/media'; + +/** + * An abstract implementation of a button for toggling video mute. + */ +export default class AbstractVideoMuteButton extends Component<*> { + /** + * {@code AbstractVideoMuteButton} component's property types. + * + * @static + */ + static propTypes = { + /** + * Whether or not the local camera is muted. + */ + _videoMuted: PropTypes.bool, + + /** + * Invoked to toggle video mute. + */ + dispatch: PropTypes.func + }; + + /** + * Initializes a new {@code AbstractVideoMuteButton} instance. + * + * @param {Props} props - The read-only React {@code Component} props with + * which the new instance is to be initialized. + */ + constructor(props: Object) { + super(props); + + // Bind event handler so it is only bound once per instance. + this._onToolbarToggleVideo = this._onToolbarToggleVideo.bind(this); + } + + /** + * Dispatches an action to toggle the mute state of the video/camera. + * + * @private + * @returns {void} + */ + _doToggleVideo() { + // The user sees the reality i.e. the state of base/tracks and intends + // to change reality by tapping on the respective button i.e. the user + // sets the state of base/media. Whether the user's intention will turn + // into reality is a whole different story which is of no concern to the + // tapping. + this.props.dispatch( + setVideoMuted( + !this.props._videoMuted, + VIDEO_MUTISM_AUTHORITY.USER, + /* ensureTrack */ true)); + } + + + _onToolbarToggleVideo: () => void; + + /** + * Creates an analytics toolbar event and dispatches an action for toggling + * video mute. + * + * @private + * @returns {void} + */ + _onToolbarToggleVideo() { + sendAnalytics(createToolbarEvent( + VIDEO_MUTE, + { + enable: !this.props._videoMuted + })); + + this._doToggleVideo(); + } +} diff --git a/react/features/toolbox/components/buttons/AudioMuteButton.native.js b/react/features/toolbox/components/buttons/AudioMuteButton.native.js new file mode 100644 index 000000000..b866ad82a --- /dev/null +++ b/react/features/toolbox/components/buttons/AudioMuteButton.native.js @@ -0,0 +1,73 @@ +// @flow + +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; + +import { MEDIA_TYPE } from '../../../base/media'; +import { isLocalTrackMuted } from '../../../base/tracks'; + +import AbstractAudioMuteButton from './AbstractAudioMuteButton'; +import ToolbarButton from '../ToolbarButton'; + +/** + * Component that renders a toolbar button for toggling audio mute. + * + * @extends AbstractAudioMuteButton + */ +export class AudioMuteButton extends AbstractAudioMuteButton { + /** + * {@code AbstractAudioMuteButton} component's property types. + * + * @static + */ + static propTypes = { + ...AbstractAudioMuteButton.propTypes, + + /** + * Styles to be applied to the button and the icon to show. + */ + buttonStyles: PropTypes.object + }; + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { buttonStyles } = this.props; + + return ( + + ); + } + + _onToolbarToggleAudio: () => void; +} + +/** + * Maps (parts of) the Redux state to the associated props for the + * {@code AudioMuteButton} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _audioMuted: boolean, + * }} + */ +function _mapStateToProps(state) { + const tracks = state['features/base/tracks']; + + return { + _audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO) + }; +} + + +export default connect(_mapStateToProps)(AudioMuteButton); diff --git a/react/features/toolbox/components/buttons/AudioMuteButton.web.js b/react/features/toolbox/components/buttons/AudioMuteButton.web.js new file mode 100644 index 000000000..27f8b6634 --- /dev/null +++ b/react/features/toolbox/components/buttons/AudioMuteButton.web.js @@ -0,0 +1,166 @@ +// @flow + +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; + +import { + ACTION_SHORTCUT_TRIGGERED, + AUDIO_MUTE, + createShortcutEvent, + sendAnalytics +} from '../../../analytics'; +import { translate } from '../../../base/i18n'; +import { MEDIA_TYPE } from '../../../base/media'; +import { isLocalTrackMuted } from '../../../base/tracks'; + +import AbstractAudioMuteButton from './AbstractAudioMuteButton'; +import ToolbarButtonV2 from '../ToolbarButtonV2'; + +declare var APP: Object; + +/** + * Component that renders a toolbar button for toggling audio mute. + * + * @extends Component + */ +export class AudioMuteButton extends AbstractAudioMuteButton { + /** + * Default values for {@code AudioMuteButton} component's properties. + * + * @static + */ + static defaultProps = { + tooltipPosition: 'top' + }; + + /** + * {@code AudioMuteButton} component's property types. + * + * @static + */ + static propTypes = { + ...AbstractAudioMuteButton.propTypes, + + /** + * The {@code JitsiConference} for the current conference. + */ + _conference: PropTypes.object, + + /** + * Invoked to update the audio mute status. + */ + dispatch: PropTypes.func, + + /** + * Invoked to obtain translated strings. + */ + t: PropTypes.func, + + /** + * Where the tooltip should display, relative to the button. + */ + tooltipPosition: PropTypes.string + }; + + /** + * Initializes a new {@code AudioMuteButton} instance. + * + * @param {Props} props - The read-only React {@code Component} props with + * which the new instance is to be initialized. + */ + constructor(props: Object) { + super(props); + + // Bind event handlers so it is only bound once per instance. + this._onShortcutToggleAudio = this._onShortcutToggleAudio.bind(this); + } + + /** + * Sets a keyboard shortcuts for toggling audio mute. + * + * @inheritdoc + * @returns {void} + */ + componentDidMount() { + APP.keyboardshortcut.registerShortcut( + 'M', + null, + this._onShortcutToggleAudio, + 'keyboardShortcuts.mute'); + } + + /** + * Removes the registered keyboard shortcut handler. + * + * @inheritdoc + * @returns {void} + */ + componentWillUnmount() { + APP.keyboardshortcut.unregisterShortcut('M'); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _audioMuted, _conference, t, tooltipPosition } = this.props; + + return ( + + ); + } + + _doToggleAudio: () => void; + + _onShortcutToggleAudio: () => void; + + /** + * Creates an analytics keyboard shortcut event and dispatches an action for + * toggling audio mute. + * + * @private + * @returns {void} + */ + _onShortcutToggleAudio() { + sendAnalytics(createShortcutEvent( + AUDIO_MUTE, + ACTION_SHORTCUT_TRIGGERED, + { enable: !this.props._audioMuted })); + + this._doToggleAudio(); + } + + _onToolbarToggleAudio: () => void; +} + +/** + * Maps (parts of) the Redux state to the associated props for the + * {@code AudioMuteButton} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _audioMuted: boolean, + * _conference: Object, + * }} + */ +function _mapStateToProps(state) { + const tracks = state['features/base/tracks']; + + return { + _audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO), + _conference: state['features/base/conference'].conference + }; +} + +export default translate(connect(_mapStateToProps)(AudioMuteButton)); diff --git a/react/features/toolbox/components/buttons/HangupButton.native.js b/react/features/toolbox/components/buttons/HangupButton.native.js new file mode 100644 index 000000000..d4d23f7b0 --- /dev/null +++ b/react/features/toolbox/components/buttons/HangupButton.native.js @@ -0,0 +1,63 @@ +// @flow + +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; + +import { appNavigate } from '../../../app'; +import { ColorPalette } from '../../../base/styles'; + +import AbstractHangupButton from './AbstractHangupButton'; +import ToolbarButton from '../ToolbarButton'; +import styles from '../styles'; + +/** + * Component that renders a toolbar button for leaving the current conference. + * + * @extends Component + */ +class HangupButton extends AbstractHangupButton { + /** + * {@code HangupButton} component's property types. + * + * @static + */ + static propTypes = { + /** + * Invoked to leave the conference. + */ + dispatch: PropTypes.func + }; + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + return ( + + ); + } + + /** + * Dispatches an action for leaving the current conference. + * + * @private + * @returns {void} + */ + _doHangup() { + this.props.dispatch(appNavigate(undefined)); + } + + _onToolbarHangup: () => void; +} + +export default connect()(HangupButton); diff --git a/react/features/toolbox/components/buttons/HangupButton.web.js b/react/features/toolbox/components/buttons/HangupButton.web.js new file mode 100644 index 000000000..fd883488b --- /dev/null +++ b/react/features/toolbox/components/buttons/HangupButton.web.js @@ -0,0 +1,82 @@ +// @flow + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; + +import { disconnect } from '../../../base/connection'; +import { translate } from '../../../base/i18n'; + +import AbstractHangupButton from './AbstractHangupButton'; +import ToolbarButtonV2 from '../ToolbarButtonV2'; + +/** + * Component that renders a toolbar button for leaving the current conference. + * + * @extends Component + */ +export class HangupButton extends AbstractHangupButton { + /** + * Default values for {@code HangupButton} component's properties. + * + * @static + */ + static defaultProps = { + tooltipPosition: 'top' + }; + + /** + * {@code HangupButton} component's property types. + * + * @static + */ + static propTypes = { + /** + * Invoked to trigger conference leave. + */ + dispatch: PropTypes.func, + + /** + * Invoked to obtain translated strings. + */ + t: PropTypes.func, + + /** + * Where the tooltip should display, relative to the button. + */ + tooltipPosition: PropTypes.string + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { t, tooltipPosition } = this.props; + + return ( + + ); + } + + _onToolbarHangup: () => void; + + /** + * Dispatches an action for leaving the current conference. + * + * @private + * @returns {void} + */ + _doHangup() { + this.props.dispatch(disconnect(true)); + } +} + +export default translate(connect()(HangupButton)); diff --git a/react/features/toolbox/components/buttons/VideoMuteButton.native.js b/react/features/toolbox/components/buttons/VideoMuteButton.native.js new file mode 100644 index 000000000..313e9ac02 --- /dev/null +++ b/react/features/toolbox/components/buttons/VideoMuteButton.native.js @@ -0,0 +1,82 @@ +// @flow + +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; + +import { MEDIA_TYPE } from '../../../base/media'; +import { isLocalTrackMuted } from '../../../base/tracks'; + +import AbstractVideoMuteButton from './AbstractVideoMuteButton'; +import ToolbarButton from '../ToolbarButton'; + +/** + * Component that renders a toolbar button for toggling video mute. + * + * @extends AbstractVideoMuteButton + */ +class VideoMuteButton extends AbstractVideoMuteButton { + /** + * {@code VideoMuteButton} component's property types. + * + * @static + */ + static propTypes = { + ...AbstractVideoMuteButton.propTypes, + + /** + * Whether or not the local participant is current in audio only mode. + * Video mute toggling is disabled in audio only mode. + */ + _audioOnly: PropTypes.bool, + + /** + * Styles to be applied to the button and the icon to show. + */ + buttonStyles: PropTypes.object + }; + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _audioOnly, buttonStyles } = this.props; + + return ( + + ); + } + + _onToolbarToggleVideo: () => void; +} + +/** + * Maps (parts of) the Redux state to the associated props for the + * {@code VideoMuteButton} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _audioOnly: boolean, + * _videoMuted: boolean + * }} + */ +function _mapStateToProps(state) { + const conference = state['features/base/conference']; + const tracks = state['features/base/tracks']; + + return { + _audioOnly: Boolean(conference.audioOnly), + _videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO) + }; +} + +export default connect(_mapStateToProps)(VideoMuteButton); diff --git a/react/features/toolbox/components/buttons/VideoMuteButton.web.js b/react/features/toolbox/components/buttons/VideoMuteButton.web.js new file mode 100644 index 000000000..f6afe97d1 --- /dev/null +++ b/react/features/toolbox/components/buttons/VideoMuteButton.web.js @@ -0,0 +1,161 @@ +// @flow + +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; + +import { + ACTION_SHORTCUT_TRIGGERED, + VIDEO_MUTE, + createShortcutEvent, + sendAnalytics +} from '../../../analytics'; +import { translate } from '../../../base/i18n'; +import { MEDIA_TYPE } from '../../../base/media'; +import { isLocalTrackMuted } from '../../../base/tracks'; + +import AbstractVideoMuteButton from './AbstractVideoMuteButton'; +import ToolbarButtonV2 from '../ToolbarButtonV2'; + +declare var APP: Object; + +/** + * Component that renders a toolbar button for toggling video mute. + * + * @extends AbstractVideoMuteButton + */ +export class VideoMuteButton extends AbstractVideoMuteButton { + /** + * Default values for {@code VideoMuteButton} component's properties. + * + * @static + */ + static defaultProps = { + tooltipPosition: 'top' + }; + + /** + * {@code VideoMuteButton} component's property types. + * + * @static + */ + static propTypes = { + ...AbstractVideoMuteButton.propTypes, + + /** + * The {@code JitsiConference} for the current conference. + */ + _conference: PropTypes.object, + + /** + * Invoked to obtain translated strings. + */ + t: PropTypes.func, + + /** + * Where the tooltip should display, relative to the button. + */ + tooltipPosition: PropTypes.string + }; + + /** + * Initializes a new {@code VideoMuteButton} instance. + * + * @param {Props} props - The read-only React {@code Component} props with + * which the new instance is to be initialized. + */ + constructor(props: Object) { + super(props); + + // Bind event handlers so they are only bound once per instance. + this._onShortcutToggleVideo = this._onShortcutToggleVideo.bind(this); + } + + /** + * Sets a keyboard shortcuts for toggling video mute. + * + * @inheritdoc + * @returns {void} + */ + componentDidMount() { + APP.keyboardshortcut.registerShortcut( + 'V', + null, + this._onShortcutToggleVideo, + 'keyboardShortcuts.videoMute'); + } + + /** + * Removes the registered keyboard shortcut handler. + * + * @inheritdoc + * @returns {void} + */ + componentWillUnmount() { + APP.keyboardshortcut.unregisterShortcut('V'); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _conference, _videoMuted, t, tooltipPosition } = this.props; + + return ( + + ); + } + + _doToggleVideo: () => void; + + _onShortcutToggleVideo: () => void; + + /** + * Creates an analytics keyboard shortcut event for and dispatches an action + * for toggling video mute. + * + * @private + * @returns {void} + */ + _onShortcutToggleVideo() { + sendAnalytics(createShortcutEvent( + VIDEO_MUTE, + ACTION_SHORTCUT_TRIGGERED, + { enable: !this.props._videoMuted })); + + this._doToggleVideo(); + } + + _onToolbarToggleVideo: () => void; +} + +/** + * Maps (parts of) the Redux state to the associated props for the + * {@code AudioMuteButton} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _conference: Object, + * _videoMuted: boolean, + * }} + */ +function _mapStateToProps(state) { + const tracks = state['features/base/tracks']; + + return { + _conference: state['features/base/conference'].conference, + _videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO) + }; +} + +export default translate(connect(_mapStateToProps)(VideoMuteButton)); diff --git a/react/features/toolbox/components/buttons/index.js b/react/features/toolbox/components/buttons/index.js new file mode 100644 index 000000000..fb4d9d327 --- /dev/null +++ b/react/features/toolbox/components/buttons/index.js @@ -0,0 +1,3 @@ +export { default as AudioMuteButton } from './AudioMuteButton'; +export { default as HangupButton } from './HangupButton'; +export { default as VideoMuteButton } from './VideoMuteButton'; diff --git a/react/features/toolbox/components/index.js b/react/features/toolbox/components/index.js index 0862cd09b..25b4f4eb1 100644 --- a/react/features/toolbox/components/index.js +++ b/react/features/toolbox/components/index.js @@ -1,4 +1,7 @@ export { default as ToolbarButton } from './ToolbarButton'; +export { default as ToolbarButtonV2 } from './ToolbarButtonV2'; export { default as ToolbarButtonWithDialog } from './ToolbarButtonWithDialog'; export { default as Toolbox } from './Toolbox'; +export { default as ToolboxFilmstrip } from './ToolboxFilmstrip'; +export { default as ToolboxV2 } from './ToolboxV2'; diff --git a/react/features/toolbox/defaultToolbarButtons.web.js b/react/features/toolbox/defaultToolbarButtons.web.js index 36513ac79..3f7c2df45 100644 --- a/react/features/toolbox/defaultToolbarButtons.web.js +++ b/react/features/toolbox/defaultToolbarButtons.web.js @@ -2,6 +2,7 @@ import React from 'react'; +import { setFullScreen } from '../toolbox'; import { ACTION_SHORTCUT_TRIGGERED as TRIGGERED, AUDIO_MUTE, @@ -10,6 +11,10 @@ import { createToolbarEvent, sendAnalytics } from '../analytics'; +import { + getLocalParticipant, + participantUpdated +} from '../base/participants'; import { ParticipantCounter } from '../contact-list'; import { openDeviceSelectionDialog } from '../device-selection'; import { InfoDialogButton } from '../invite'; @@ -252,33 +257,37 @@ export default function getDefaultButtons() { enabled: true, id: 'toolbar_button_fullScreen', onClick() { - // TODO: why is the fullscreen button handled differently than - // the fullscreen keyboard shortcut (one results in a direct - // call to toggleFullScreen, while the other fires an - // UIEvents.TOGGLE_FULLSCREEN event)? + const state = APP.store.getState(); + const isFullScreen = Boolean( + state['features/toolbox'].fullScreen); // The 'enable' attribute is set to true if the action resulted // in fullscreen mode being enabled. sendAnalytics(createToolbarEvent( 'toggle.fullscreen', { - enable: !APP.UI.isFullScreen() + enable: !isFullScreen })); - APP.UI.emitEvent(UIEvents.TOGGLE_FULLSCREEN); + APP.store.dispatch(setFullScreen(!isFullScreen)); }, shortcut: 'S', shortcutAttr: 'toggleFullscreenPopover', shortcutDescription: 'keyboardShortcuts.fullScreen', shortcutFunc() { + const state = APP.store.getState(); + const isFullScreen = Boolean( + state['features/toolbox'].fullScreen); + // The 'enable' attribute is set to true if the action resulted // in fullscreen mode being enabled. sendAnalytics(createShortcutEvent( 'toggle.fullscreen', { - enable: !APP.UI.isFullScreen() + enable: !isFullScreen })); - APP.UI.toggleFullScreen(); + + APP.store.dispatch(setFullScreen(!isFullScreen)); }, tooltipKey: 'toolbar.fullscreen' }, @@ -394,27 +403,44 @@ export default function getDefaultButtons() { id: 'toolbar_button_raisehand', onClick() { // TODO: reduce duplication with shortcutFunc below. + const localParticipant + = getLocalParticipant(APP.store.getState()); + const currentRaisedHand = localParticipant.raisedHand; // The 'enable' attribute is set to true if the pressing of the // shortcut resulted in the hand being raised, and to false // if it resulted in the hand being 'lowered'. sendAnalytics(createToolbarEvent( 'raise.hand', - { enable: !APP.conference.isHandRaised })); - APP.conference.maybeToggleRaisedHand(); + { enable: !currentRaisedHand })); + + APP.store.dispatch(participantUpdated({ + id: localParticipant.id, + local: true, + raisedHand: !currentRaisedHand + })); }, shortcut: 'R', shortcutAttr: 'raiseHandPopover', shortcutDescription: 'keyboardShortcuts.raiseHand', shortcutFunc() { + const localParticipant + = getLocalParticipant(APP.store.getState()); + const currentRaisedHand = localParticipant.raisedHand; + // The 'enable' attribute is set to true if the pressing of the // shortcut resulted in the hand being raised, and to false // if it resulted in the hand being 'lowered'. sendAnalytics(createShortcutEvent( 'toggle.raise.hand', TRIGGERED, - { enable: !APP.conference.isHandRaised })); - APP.conference.maybeToggleRaisedHand(); + { enable: !currentRaisedHand })); + + APP.store.dispatch(participantUpdated({ + id: localParticipant.id, + local: true, + raisedHand: !currentRaisedHand + })); }, tooltipKey: 'toolbar.raiseHand' }, diff --git a/react/features/toolbox/functions.web.js b/react/features/toolbox/functions.web.js index 072becd61..a1b1da0cb 100644 --- a/react/features/toolbox/functions.web.js +++ b/react/features/toolbox/functions.web.js @@ -99,6 +99,17 @@ export function getToolbarClassNames(props: Object) { }; } +/** + * Helper for getting the height of the toolbox. + * + * @returns {number} The height of the toolbox. + */ +export function getToolboxHeight() { + const toolbox = document.getElementById('new-toolbox'); + + return (toolbox && toolbox.clientHeight) || 0; +} + /** * Indicates if a toolbar button is enabled. * diff --git a/react/features/toolbox/middleware.js b/react/features/toolbox/middleware.js index be1a95b85..0b9cf8fdf 100644 --- a/react/features/toolbox/middleware.js +++ b/react/features/toolbox/middleware.js @@ -1,4 +1,4 @@ -/* @flow */ +// @flow import { MEDIA_TYPE, @@ -8,8 +8,15 @@ import { import { MiddlewareRegistry } from '../base/redux'; import { isLocalTrackMuted, TRACK_UPDATED } from '../base/tracks'; -import { setToolbarButton } from './actions'; -import { CLEAR_TOOLBOX_TIMEOUT, SET_TOOLBOX_TIMEOUT } from './actionTypes'; +import { setToolbarButton, toggleFullScreen } from './actions'; +import { + CLEAR_TOOLBOX_TIMEOUT, + FULL_SCREEN_CHANGED, + SET_TOOLBOX_TIMEOUT, + SET_FULL_SCREEN +} from './actionTypes'; + +declare var APP: Object; /** * Middleware which intercepts Toolbox actions to handle changes to the @@ -27,9 +34,15 @@ MiddlewareRegistry.register(store => next => action => { break; } + case FULL_SCREEN_CHANGED: + return _fullScreenChanged(store, next, action); + case SET_AUDIO_AVAILABLE: return _setMediaAvailableOrMuted(store, next, action); + case SET_FULL_SCREEN: + return _setFullScreen(next, action); + case SET_TOOLBOX_TIMEOUT: { const { timeoutID } = store.getState()['features/toolbox']; const { handler, timeoutMS } = action; @@ -54,6 +67,26 @@ MiddlewareRegistry.register(store => next => action => { return next(action); }); +/** + * Updates the the redux state with the current known state of full screen. + * + * @param {Store} store - The redux store in which the specified action is being + * dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action FULL_SCREEN_CHANGED which is being + * dispatched in the specified store. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _fullScreenChanged({ dispatch }, next, action) { + if (typeof APP === 'object') { + dispatch(toggleFullScreen(action.fullScreen)); + } + + return next(action); +} + /** * Adjusts the state of toolbar's microphone or camera button. * @@ -110,3 +143,46 @@ function _setMediaAvailableOrMuted({ dispatch, getState }, next, action) { return result; } + +/** + * Makes an external request to enter or exit full screen mode. + * + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action SET_FULL_SCREEN which is being + * dispatched in the specified store. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _setFullScreen(next, action) { + if (typeof APP === 'object') { + const { fullScreen } = action; + + if (fullScreen) { + const documentElement = document.documentElement || {}; + + if (typeof documentElement.requestFullscreen === 'function') { + documentElement.requestFullscreen(); + } else if ( + typeof documentElement.msRequestFullscreen === 'function') { + documentElement.msRequestFullscreen(); + } else if ( + typeof documentElement.mozRequestFullScreen === 'function') { + documentElement.mozRequestFullScreen(); + } else if ( + typeof documentElement.webkitRequestFullscreen === 'function') { + documentElement.webkitRequestFullscreen(); + } + } else if (typeof document.exitFullscreen === 'function') { + document.exitFullscreen(); + } else if (typeof document.msExitFullscreen === 'function') { + document.msExitFullscreen(); + } else if (typeof document.mozCancelFullScreen === 'function') { + document.mozCancelFullScreen(); + } else if (typeof document.webkitExitFullscreen === 'function') { + document.webkitExitFullscreen(); + } + } + + return next(action); +} diff --git a/react/features/toolbox/reducer.js b/react/features/toolbox/reducer.js index 51099494f..2327faa7f 100644 --- a/react/features/toolbox/reducer.js +++ b/react/features/toolbox/reducer.js @@ -4,6 +4,7 @@ import { ReducerRegistry } from '../base/redux'; import { CLEAR_TOOLBOX_TIMEOUT, + FULL_SCREEN_CHANGED, SET_DEFAULT_TOOLBOX_BUTTONS, SET_SUBJECT, SET_SUBJECT_SLIDE_IN, @@ -132,6 +133,12 @@ ReducerRegistry.register( timeoutID: undefined }; + case FULL_SCREEN_CHANGED: + return { + ...state, + fullScreen: action.fullScreen + }; + case SET_DEFAULT_TOOLBOX_BUTTONS: { const { primaryToolbarButtons, secondaryToolbarButtons } = action; diff --git a/react/features/video-quality/components/VideoQualityButton.web.js b/react/features/video-quality/components/VideoQualityButton.web.js index f985db09d..772e930d4 100644 --- a/react/features/video-quality/components/VideoQualityButton.web.js +++ b/react/features/video-quality/components/VideoQualityButton.web.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { ToolbarButtonWithDialog } from '../../toolbox'; -import { VideoQualityDialog } from './'; +import VideoQualitySlider from './VideoQualitySlider'; /** * A configuration object to describe how {@code ToolbarButton} should render @@ -49,7 +49,7 @@ class VideoQualityButton extends Component { return ( ); } diff --git a/react/features/video-quality/components/VideoQualityDialog.web.js b/react/features/video-quality/components/VideoQualityDialog.web.js index 9383b4552..c1b2d4209 100644 --- a/react/features/video-quality/components/VideoQualityDialog.web.js +++ b/react/features/video-quality/components/VideoQualityDialog.web.js @@ -1,140 +1,16 @@ -import InlineMessage from '@atlaskit/inline-message'; -import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { - createToolbarEvent, - sendAnalytics -} from '../../analytics'; -import { - VIDEO_QUALITY_LEVELS, - setAudioOnly, - setReceiveVideoQuality -} from '../../base/conference'; -import { translate } from '../../base/i18n'; -import JitsiMeetJS from '../../base/lib-jitsi-meet'; +import { Dialog } from '../../base/dialog'; -const logger = require('jitsi-meet-logger').getLogger(__filename); - -const { - HIGH, - STANDARD, - LOW -} = VIDEO_QUALITY_LEVELS; +import VideoQualitySlider from './VideoQualitySlider'; /** - * Creates an analytics event for a press of one of the buttons in the video - * quality dialog. - * - * @param {string} quality - The quality which was selected. - * @returns {Object} The event in a format suitable for sending via - * sendAnalytics. - */ -const createEvent = function(quality) { - return createToolbarEvent( - 'video.quality', - { - quality - }); -}; - -/** - * Implements a React {@link Component} which displays a dialog with a slider - * for selecting a new receive video quality. + * Implements a React {@link Component} which displays the component + * {@code VideoQualitySlider} in a dialog. * * @extends Component */ -class VideoQualityDialog extends Component { - /** - * {@code VideoQualityDialog}'s property types. - * - * @static - */ - static propTypes = { - /** - * Whether or not the conference is in audio only mode. - */ - _audioOnly: PropTypes.bool, - - /** - * Whether or not the conference is in peer to peer mode. - */ - _p2p: PropTypes.bool, - - /** - * The currently configured maximum quality resolution to be received - * from remote participants. - */ - _receiveVideoQuality: PropTypes.number, - - /** - * Whether or not displaying video is supported in the current - * environment. If false, the slider will be disabled. - */ - _videoSupported: PropTypes.bool, - - /** - * Invoked to request toggling of audio only mode. - */ - dispatch: PropTypes.func, - - /** - * Invoked to obtain translated strings. - */ - t: PropTypes.func - }; - - /** - * Initializes a new {@code VideoQualityDialog} instance. - * - * @param {Object} props - The read-only React Component props 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._enableAudioOnly = this._enableAudioOnly.bind(this); - this._enableHighDefinition = this._enableHighDefinition.bind(this); - this._enableLowDefinition = this._enableLowDefinition.bind(this); - this._enableStandardDefinition - = this._enableStandardDefinition.bind(this); - this._onSliderChange = this._onSliderChange.bind(this); - - /** - * An array of configuration options for displaying a choice in the - * input. The onSelect callback will be invoked when the option is - * selected and videoQuality helps determine which choice matches with - * the currently active quality level. - * - * @private - * @type {Object[]} - */ - this._sliderOptions = [ - { - audioOnly: true, - onSelect: this._enableAudioOnly, - textKey: 'audioOnly.audioOnly' - }, - { - onSelect: this._enableLowDefinition, - textKey: 'videoStatus.lowDefinition', - videoQuality: LOW - }, - { - onSelect: this._enableStandardDefinition, - textKey: 'videoStatus.standardDefinition', - videoQuality: STANDARD - }, - { - onSelect: this._enableHighDefinition, - textKey: 'videoStatus.highDefinition', - videoQuality: HIGH - } - ]; - } - +export default class VideoQualityDialog extends Component { /** * Implements React's {@link Component#render()}. * @@ -142,252 +18,14 @@ class VideoQualityDialog extends Component { * @returns {ReactElement} */ render() { - const { _audioOnly, _p2p, _videoSupported, t } = this.props; - const activeSliderOption = this._mapCurrentQualityToSliderValue(); - - let classNames = 'video-quality-dialog'; - let warning = null; - - if (!_videoSupported) { - classNames += ' video-not-supported'; - warning = this._renderAudioOnlyLockedMessage(); - } else if (_p2p && !_audioOnly) { - warning = this._renderP2PMessage(); - } - return ( -
    -

    - { t('videoStatus.callQuality') } -

    -
    - { warning } -
    -
    -
    - { /* FIXME: onChange and onMouseUp are both used for - * compatibility with IE11. This workaround can be - * removed after upgrading to React 16. - */ } - - -
    -
    - { this._createLabels(activeSliderOption) } -
    -
    -
    + + + ); } - - /** - * Creates a React Element for notifying that the browser is in audio only - * and cannot be changed. - * - * @private - * @returns {ReactElement} - */ - _renderAudioOnlyLockedMessage() { - const { t } = this.props; - - return ( - - { t('videoStatus.onlyAudioSupported') } - - ); - } - - /** - * Creates React Elements for notifying that peer to peer is enabled. - * - * @private - * @returns {ReactElement} - */ - _renderP2PMessage() { - const { t } = this.props; - - return ( - - { t('videoStatus.p2pVideoQualityDescription') } - - ); - } - - /** - * Creates React Elements to display mock tick marks with associated labels. - * - * @param {number} activeLabelIndex - Which of the sliderOptions should - * display as currently active. - * @private - * @returns {ReactElement[]} - */ - _createLabels(activeLabelIndex) { - const labelsCount = this._sliderOptions.length; - const maxWidthOfLabel = `${100 / labelsCount}%`; - - return this._sliderOptions.map((sliderOption, index) => { - const style = { - maxWidth: maxWidthOfLabel, - left: `${(index * 100) / (labelsCount - 1)}%` - }; - - const isActiveClass = activeLabelIndex === index ? 'active' : ''; - const className - = `video-quality-dialog-label-container ${isActiveClass}`; - - return ( -
    -
    - { this.props.t(sliderOption.textKey) } -
    -
    - ); - }); - } - - /** - * Dispatches an action to enable audio only mode. - * - * @private - * @returns {void} - */ - _enableAudioOnly() { - sendAnalytics(createEvent('audio.only')); - logger.log('Video quality: audio only enabled'); - this.props.dispatch(setAudioOnly(true)); - } - - /** - * Handles the action of the high definition video being selected. - * Dispatches an action to receive high quality video from remote - * participants. - * - * @private - * @returns {void} - */ - _enableHighDefinition() { - sendAnalytics(createEvent('high')); - logger.log('Video quality: high enabled'); - this.props.dispatch(setReceiveVideoQuality(HIGH)); - } - - /** - * Dispatches an action to receive low quality video from remote - * participants. - * - * @private - * @returns {void} - */ - _enableLowDefinition() { - sendAnalytics(createEvent('low')); - logger.log('Video quality: low enabled'); - this.props.dispatch(setReceiveVideoQuality(LOW)); - } - - /** - * Dispatches an action to receive standard quality video from remote - * participants. - * - * @private - * @returns {void} - */ - _enableStandardDefinition() { - sendAnalytics(createEvent('standard')); - logger.log('Video quality: standard enabled'); - this.props.dispatch(setReceiveVideoQuality(STANDARD)); - } - - /** - * Matches the current video quality state with corresponding index of the - * component's slider options. - * - * @private - * @returns {void} - */ - _mapCurrentQualityToSliderValue() { - const { _audioOnly, _receiveVideoQuality } = this.props; - const { _sliderOptions } = this; - - if (_audioOnly) { - const audioOnlyOption = _sliderOptions.find( - ({ audioOnly }) => audioOnly); - - return _sliderOptions.indexOf(audioOnlyOption); - } - - const matchingOption = _sliderOptions.find( - ({ videoQuality }) => videoQuality === _receiveVideoQuality); - - return _sliderOptions.indexOf(matchingOption); - } - - /** - * Invokes a callback when the selected video quality changes. - * - * @param {Object} event - The slider's change event. - * @private - * @returns {void} - */ - _onSliderChange(event) { - const { _audioOnly, _receiveVideoQuality } = this.props; - const { - audioOnly, - onSelect, - videoQuality - } = this._sliderOptions[event.target.value]; - - // Take no action if the newly chosen option does not change audio only - // or video quality state. - if ((_audioOnly && audioOnly) - || (!_audioOnly && videoQuality === _receiveVideoQuality)) { - return; - } - - onSelect(); - } } - -/** - * Maps (parts of) the Redux state to the associated props for the - * {@code VideoQualityDialog} component. - * - * @param {Object} state - The Redux state. - * @private - * @returns {{ - * _audioOnly: boolean, - * _p2p: boolean, - * _receiveVideoQuality: boolean - * }} - */ -function _mapStateToProps(state) { - const { - audioOnly, - p2p, - receiveVideoQuality - } = state['features/base/conference']; - - return { - _audioOnly: audioOnly, - _p2p: p2p, - _receiveVideoQuality: receiveVideoQuality, - _videoSupported: JitsiMeetJS.mediaDevices.supportsVideo() - }; -} - -export default translate(connect(_mapStateToProps)(VideoQualityDialog)); diff --git a/react/features/video-quality/components/VideoQualitySlider.native.js b/react/features/video-quality/components/VideoQualitySlider.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/video-quality/components/VideoQualitySlider.web.js b/react/features/video-quality/components/VideoQualitySlider.web.js new file mode 100644 index 000000000..9a39586c0 --- /dev/null +++ b/react/features/video-quality/components/VideoQualitySlider.web.js @@ -0,0 +1,393 @@ +import InlineMessage from '@atlaskit/inline-message'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { + createToolbarEvent, + sendAnalytics +} from '../../analytics'; +import { + VIDEO_QUALITY_LEVELS, + setAudioOnly, + setReceiveVideoQuality +} from '../../base/conference'; +import { translate } from '../../base/i18n'; +import JitsiMeetJS from '../../base/lib-jitsi-meet'; + +const logger = require('jitsi-meet-logger').getLogger(__filename); + +const { + HIGH, + STANDARD, + LOW +} = VIDEO_QUALITY_LEVELS; + +/** + * Creates an analytics event for a press of one of the buttons in the video + * quality dialog. + * + * @param {string} quality - The quality which was selected. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. + */ +const createEvent = function(quality) { + return createToolbarEvent( + 'video.quality', + { + quality + }); +}; + +/** + * Implements a React {@link Component} which displays a slider for selecting a + * new receive video quality. + * + * @extends Component + */ +class VideoQualitySlider extends Component { + /** + * {@code VideoQualitySlider}'s property types. + * + * @static + */ + static propTypes = { + /** + * Whether or not the conference is in audio only mode. + */ + _audioOnly: PropTypes.bool, + + /** + * Whether or not the conference is in peer to peer mode. + */ + _p2p: PropTypes.bool, + + /** + * The currently configured maximum quality resolution to be received + * from remote participants. + */ + _receiveVideoQuality: PropTypes.number, + + /** + * Whether or not displaying video is supported in the current + * environment. If false, the slider will be disabled. + */ + _videoSupported: PropTypes.bool, + + /** + * Invoked to request toggling of audio only mode. + */ + dispatch: PropTypes.func, + + /** + * Invoked to obtain translated strings. + */ + t: PropTypes.func + }; + + /** + * Initializes a new {@code VideoQualitySlider} instance. + * + * @param {Object} props - The read-only React Component props 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._enableAudioOnly = this._enableAudioOnly.bind(this); + this._enableHighDefinition = this._enableHighDefinition.bind(this); + this._enableLowDefinition = this._enableLowDefinition.bind(this); + this._enableStandardDefinition + = this._enableStandardDefinition.bind(this); + this._onSliderChange = this._onSliderChange.bind(this); + + /** + * An array of configuration options for displaying a choice in the + * input. The onSelect callback will be invoked when the option is + * selected and videoQuality helps determine which choice matches with + * the currently active quality level. + * + * @private + * @type {Object[]} + */ + this._sliderOptions = [ + { + audioOnly: true, + onSelect: this._enableAudioOnly, + textKey: 'audioOnly.audioOnly' + }, + { + onSelect: this._enableLowDefinition, + textKey: 'videoStatus.lowDefinition', + videoQuality: LOW + }, + { + onSelect: this._enableStandardDefinition, + textKey: 'videoStatus.standardDefinition', + videoQuality: STANDARD + }, + { + onSelect: this._enableHighDefinition, + textKey: 'videoStatus.highDefinition', + videoQuality: HIGH + } + ]; + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _audioOnly, _p2p, _videoSupported, t } = this.props; + const activeSliderOption = this._mapCurrentQualityToSliderValue(); + + let classNames = 'video-quality-dialog'; + let warning = null; + + if (!_videoSupported) { + classNames += ' video-not-supported'; + warning = this._renderAudioOnlyLockedMessage(); + } else if (_p2p && !_audioOnly) { + warning = this._renderP2PMessage(); + } + + return ( +
    +

    + { t('videoStatus.callQuality') } +

    +
    + { warning } +
    +
    +
    + { /* FIXME: onChange and onMouseUp are both used for + * compatibility with IE11. This workaround can be + * removed after upgrading to React 16. + */ } + + +
    +
    + { this._createLabels(activeSliderOption) } +
    +
    +
    + ); + } + + /** + * Creates a React Element for notifying that the browser is in audio only + * and cannot be changed. + * + * @private + * @returns {ReactElement} + */ + _renderAudioOnlyLockedMessage() { + const { t } = this.props; + + return ( + + { t('videoStatus.onlyAudioSupported') } + + ); + } + + /** + * Creates React Elements for notifying that peer to peer is enabled. + * + * @private + * @returns {ReactElement} + */ + _renderP2PMessage() { + const { t } = this.props; + + return ( + + { t('videoStatus.p2pVideoQualityDescription') } + + ); + } + + /** + * Creates React Elements to display mock tick marks with associated labels. + * + * @param {number} activeLabelIndex - Which of the sliderOptions should + * display as currently active. + * @private + * @returns {ReactElement[]} + */ + _createLabels(activeLabelIndex) { + const labelsCount = this._sliderOptions.length; + const maxWidthOfLabel = `${100 / labelsCount}%`; + + return this._sliderOptions.map((sliderOption, index) => { + const style = { + maxWidth: maxWidthOfLabel, + left: `${(index * 100) / (labelsCount - 1)}%` + }; + + const isActiveClass = activeLabelIndex === index ? 'active' : ''; + const className + = `video-quality-dialog-label-container ${isActiveClass}`; + + return ( +
    +
    + { this.props.t(sliderOption.textKey) } +
    +
    + ); + }); + } + + /** + * Dispatches an action to enable audio only mode. + * + * @private + * @returns {void} + */ + _enableAudioOnly() { + sendAnalytics(createEvent('audio.only')); + logger.log('Video quality: audio only enabled'); + this.props.dispatch(setAudioOnly(true)); + } + + /** + * Handles the action of the high definition video being selected. + * Dispatches an action to receive high quality video from remote + * participants. + * + * @private + * @returns {void} + */ + _enableHighDefinition() { + sendAnalytics(createEvent('high')); + logger.log('Video quality: high enabled'); + this.props.dispatch(setReceiveVideoQuality(HIGH)); + } + + /** + * Dispatches an action to receive low quality video from remote + * participants. + * + * @private + * @returns {void} + */ + _enableLowDefinition() { + sendAnalytics(createEvent('low')); + logger.log('Video quality: low enabled'); + this.props.dispatch(setReceiveVideoQuality(LOW)); + } + + /** + * Dispatches an action to receive standard quality video from remote + * participants. + * + * @private + * @returns {void} + */ + _enableStandardDefinition() { + sendAnalytics(createEvent('standard')); + logger.log('Video quality: standard enabled'); + this.props.dispatch(setReceiveVideoQuality(STANDARD)); + } + + /** + * Matches the current video quality state with corresponding index of the + * component's slider options. + * + * @private + * @returns {void} + */ + _mapCurrentQualityToSliderValue() { + const { _audioOnly, _receiveVideoQuality } = this.props; + const { _sliderOptions } = this; + + if (_audioOnly) { + const audioOnlyOption = _sliderOptions.find( + ({ audioOnly }) => audioOnly); + + return _sliderOptions.indexOf(audioOnlyOption); + } + + const matchingOption = _sliderOptions.find( + ({ videoQuality }) => videoQuality === _receiveVideoQuality); + + return _sliderOptions.indexOf(matchingOption); + } + + /** + * Invokes a callback when the selected video quality changes. + * + * @param {Object} event - The slider's change event. + * @private + * @returns {void} + */ + _onSliderChange(event) { + const { _audioOnly, _receiveVideoQuality } = this.props; + const { + audioOnly, + onSelect, + videoQuality + } = this._sliderOptions[event.target.value]; + + // Take no action if the newly chosen option does not change audio only + // or video quality state. + if ((_audioOnly && audioOnly) + || (!_audioOnly && videoQuality === _receiveVideoQuality)) { + return; + } + + onSelect(); + } +} + +/** + * Maps (parts of) the Redux state to the associated props for the + * {@code VideoQualitySlider} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _audioOnly: boolean, + * _p2p: boolean, + * _receiveVideoQuality: boolean + * }} + */ +function _mapStateToProps(state) { + const { + audioOnly, + p2p, + receiveVideoQuality + } = state['features/base/conference']; + + return { + _audioOnly: audioOnly, + _p2p: p2p, + _receiveVideoQuality: receiveVideoQuality, + _videoSupported: JitsiMeetJS.mediaDevices.supportsVideo() + }; +} + +export default translate(connect(_mapStateToProps)(VideoQualitySlider)); diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index dcaed5720..fb99426d9 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -66,7 +66,7 @@ export default { * @see {TOGGLE_FILMSTRIP} */ TOGGLED_FILMSTRIP: 'UI.toggled_filmstrip', - + TOGGLE_RECORDING: 'UI.toggle_recording', TOGGLE_SCREENSHARING: 'UI.toggle_screensharing', TOGGLED_SHARED_DOCUMENT: 'UI.toggled_shared_document', CONTACT_CLICKED: 'UI.contact_clicked',