diff --git a/css/_connection-info.scss b/css/_connection-info.scss index eb0d406c4..6527cba5c 100644 --- a/css/_connection-info.scss +++ b/css/_connection-info.scss @@ -1,8 +1,7 @@ %connection-info { - text-align: left; font-size: 12px; font-weight: 400; - color: $popoverFontColor; + color: $modalTextColor; td { padding: 2px 0; @@ -11,11 +10,14 @@ .connection-info { - float: left; - padding: 5px; - padding-left: 0; @extend %connection-info; + /** + * Apply negative margin to reduce the appearance of padding in AtlasKit + * InlineDialog. + */ + margin: -15px; + > table { white-space: nowrap; @extend %connection-info; @@ -40,4 +42,11 @@ @extend .connection-info__icon; color: $uploadConnectionIconColor; } + + .showmore { + display: block; + margin: 10px auto; + text-align: center; + width: 90px; + } } diff --git a/css/_jitsi_popover.scss b/css/_jitsi_popover.scss deleted file mode 100644 index f10a25a69..000000000 --- a/css/_jitsi_popover.scss +++ /dev/null @@ -1,92 +0,0 @@ -.jitsipopover { - position: absolute; - top: 0; - left: 0; - z-index: $jitsipopoverZ; - display: table; - visibility: hidden; - max-width: 300px; - min-width: 100px; - text-align: left; - color: $popoverFontColor; - background-color: $popoverBg; - background-clip: padding-box; - border-radius: $borderRadius; - /*-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);*/ - /*box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4);*/ - white-space: normal; - margin-top: -$popoverMenuPadding; - - - &__menu-padding, - &__menu-padding-top { - position: absolute; - width: 100px; - } - - /** - * Invisible padding is added to the bottom of the popover to extend its - * height so it does not close when moving the mouse from the trigger - * element towards the popover itself. - */ - &__menu-padding { - bottom: -$popoverMenuPadding; - height: $popoverMenuPadding; - } - - /** - * Invisible padding is added to the top of the popover to extend its height - * so it does not close automatically when its height is shrunk from showing - * less video statistics. - */ - &__menu-padding-top { - height: 20px; - top: -20px; - } - - &__showmore { - display: block; - text-align: center; - width: 90px; - margin: 10px auto; - } - - > .arrow { - position: absolute; - display: block; - left: 50%; - bottom: -5px; - margin-left: -5px; - width: 0; - height: 0; - border-color: transparent; - border-top-color: $popoverBg; - border-style: solid; - border-width: 5px; - border-bottom-width: 0; - } - - /** - * Override default "top" styles to support popovers appearing from the - * left of the popover trigger element. - */ - &.left { - margin-left: -$popoverMenuPadding; - margin-top: 0; - - .arrow { - border-color: transparent transparent transparent $popoverBg; - border-width: 5px 0px 5px 5px; - margin-left: 0; - margin-top: -5px; - } - - .jitsipopover { - &__menu-padding { - bottom: 0; - height: 100%; - width: $popoverMenuPadding; - } - } - } -} diff --git a/css/_popup_menu.scss b/css/_popup_menu.scss index 9b7eb0064..7b34486a5 100644 --- a/css/_popup_menu.scss +++ b/css/_popup_menu.scss @@ -3,37 +3,31 @@ **/ .popupmenu { + text-align: left; padding: 0; - margin: 2px 0; - bottom: 0; - height: auto; - - &:first-child { - margin-top: 2px; - } + white-space: nowrap; &__item { list-style-type: none; - text-align: left; height: 35px; &:hover { - background-color: $popupMenuSelectedItemBackground; + background-color: rgba(9, 30, 66, 0.04); } } // Link Appearance &__link, &__contents { + color: $modalTextColor; display: block; box-sizing: border-box; text-decoration: none; - color: #fff; - padding: 5px; height: 100%; font-size: 9pt; width: 100%; - cursor: hand; + cursor: pointer; + padding: 0 5px; &.disabled { color: gray !important; @@ -46,6 +40,12 @@ vertical-align: middle; } + &__link { + i { + cursor: pointer; + } + } + &__contents { display: flex; @@ -73,7 +73,6 @@ display: inline-block; min-width: 20px; height: 100%; - text-align: center; > * { @include absoluteAligning(); @@ -85,6 +84,15 @@ } } +/** + * Override reset css styling modifying all lists and set negative margin to + * reduce the visibility of padding on AtlasKit + * InlineDialogs. + */ +ul.popupmenu { + margin: -15px; +} + span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover { display:block !important; } diff --git a/css/_variables.scss b/css/_variables.scss index 2cf78565f..479ab9b30 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -112,7 +112,6 @@ $tooltipsZ: 401; $dropdownMaskZ: 900; $dropdownZ: 901; $centeredVideoLabelZ: 1010; -$jitsipopoverZ: 1012; $popoverZ: 1015; $overlayZ: 1016; diff --git a/css/_vertical_filmstrip_overrides.scss b/css/_vertical_filmstrip_overrides.scss index 434931d49..08aad336d 100644 --- a/css/_vertical_filmstrip_overrides.scss +++ b/css/_vertical_filmstrip_overrides.scss @@ -8,6 +8,14 @@ display: flex; flex-direction: column-reverse; height: 100%; + /** + * fixed positioning is necessary for remote menus and tooltips to pop + * out of the scrolling filmstrip. AtlasKit dialogs and tooltips use + * a library called popper which will position its elements fixed if + * any parent is also fixed. + */ + position: fixed; + z-index: $tooltipsZ; /** * Hide videos by making them slight to the right. @@ -60,13 +68,16 @@ * Move the remote video menu trigger to the bottom left of the * video thumbnail. */ - .remotevideomenu { + .remotevideomenu, + .remote-video-menu-trigger { bottom: 0; left: 0; top: auto; right: auto; + } + + .remote-video-menu-trigger { margin-bottom: 7px; - transform: translate3d(0,0,0); } #remoteVideos { @@ -75,11 +86,6 @@ } .videocontainer { - &__toolbar, - &__toptoolbar { - transform: translate3d(0,0,0); - } - /** * Move status icons to the bottom right of the thumbnail. */ @@ -159,4 +165,17 @@ transition-delay: 0.1s; } } + + /** + * Apply hardware acceleration to prevent flickering on scroll. The + * selectors are specific to icon wrappers to prevent fixed position dialogs + * and tooltips from getting a new location context due to translate3d. + */ + .connection-indicator, + .remote-video-menu-trigger, + .videocontainer__toolbar, + .raisehandindicator, + #dominantspeakerindicator { + transform: translate3d(0, 0, 0); + } } diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index d64632c3f..6c8503e28 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -48,15 +48,30 @@ &__toolbar { bottom: 0; - padding: 0 5px 0 5px; height: $thumbnailToolbarHeight; + padding: 0 5px 0 5px; } &__toptoolbar { - $toolbarPadding: 5px; + $toolbarIconMargin: 5px; top: 0; - padding: $toolbarPadding; padding-bottom: 0; + /** + * Override text-align center as icons need to be left justified. + */ + text-align: left; + + /** + * Intentionally use margin on the icon itself as AtlasKit InlineDialog + * positioning depends on the trigger (indicator icon). + */ + .indicator { + margin-top: $toolbarIconMargin; + } + + .indicator:nth-child(1) { + margin-left: $toolbarIconMargin; + } .connection-indicator, span.indicator { @@ -71,6 +86,15 @@ } } + .connection-indicator-container { + display: inline-block; + vertical-align: top; + + .popover-trigger { + display: inline-block; + } + } + .connection-indicator, span.indicator { position: relative; @@ -78,7 +102,6 @@ text-align: center; line-height: $thumbnailIndicatorSize; padding: 0; - float: left; @include circle($thumbnailIndicatorSize); box-sizing: border-box; z-index: $zindex3; @@ -124,6 +147,7 @@ .icon-connection, .icon-connection-lost { + cursor: pointer; font-size: 1em; } } @@ -309,13 +333,13 @@ background: $connectionIndicatorBg; } +.remote-video-menu-trigger, .remotevideomenu { display: inline-block; position: absolute; top: 0px; right: 0; - margin-top: 7px; z-index: $zindex3; width: 18px; height: 13px; @@ -326,6 +350,9 @@ cursor: hand; } } +.remote-video-menu-trigger { + margin-top: 7px; +} /** * Audio indicator on video thumbnails. diff --git a/css/main.scss b/css/main.scss index 21cda1343..578b2a3b5 100644 --- a/css/main.scss +++ b/css/main.scss @@ -50,7 +50,6 @@ @import 'recording'; @import 'login_menu'; @import 'popover'; -@import 'jitsi_popover'; @import 'contact_list'; @import 'chat'; @import 'ringing/ringing'; diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 336625810..f172d7f3d 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -8,7 +8,6 @@ import Chat from "./side_pannels/chat/Chat"; import SidePanels from "./side_pannels/SidePanels"; import Avatar from "./avatar/Avatar"; import SideContainerToggler from "./side_pannels/SideContainerToggler"; -import JitsiPopover from "./util/JitsiPopover"; import messageHandler from "./util/MessageHandler"; import UIUtil from "./util/UIUtil"; import { activateTooltips } from './util/Tooltip'; @@ -322,7 +321,6 @@ UI.start = function () { UI.showToolbar(); Filmstrip.setFilmstripOnly(); APP.store.dispatch(setNotificationsEnabled(false)); - JitsiPopover.enabled = false; } if (interfaceConfig.VERTICAL_FILMSTRIP) { diff --git a/modules/UI/util/JitsiPopover.js b/modules/UI/util/JitsiPopover.js deleted file mode 100644 index 94466a01c..000000000 --- a/modules/UI/util/JitsiPopover.js +++ /dev/null @@ -1,276 +0,0 @@ -/* global $ */ - -/* eslint-disable no-unused-vars */ -import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; -import { I18nextProvider } from 'react-i18next'; - -import { i18next } from '../../../react/features/base/i18n'; -/* eslint-enable no-unused-vars */ - -const positionConfigurations = { - left: { - - // Align the popover's right side to the target element. - my: 'right', - - // Align the popover to the left side of the target element. - at: 'left', - - // Force the popover to fit within the viewport. - collision: 'fit', - - /** - * Callback invoked by jQuery UI tooltip. - * - * @param {Object} position - The top and bottom position the popover - * element should be set at. - * @param {Object} element. - Additional size and position information - * about the popover element and target. - * @param {Object} elements.element - Has position and size related data - * for the popover element itself. - * @param {Object} elements.target - Has position and size related data - * for the target element the popover displays from. - */ - using: function setPositionLeft(position, elements) { - const { element, target } = elements; - - $('.jitsipopover').css({ - left: element.left, - top: element.top, - visibility: 'visible' - }); - - // Move additional padding to the right edge of the popover and - // allow css to take care of width. The padding is used to maintain - // a hover state between the target and the popover. - $('.jitsipopover > .jitsipopover__menu-padding') - .css({ left: element.width }); - - // Find the distance from the top of the popover to the center of - // the target and use that value to position the arrow to point to - // it. - const verticalCenterOfTarget = target.height / 2; - const verticalDistanceFromTops = target.top - element.top; - const verticalPositionOfTargetCenter - = verticalDistanceFromTops + verticalCenterOfTarget; - - $('.jitsipopover > .arrow').css({ - left: element.width, - top: verticalPositionOfTargetCenter - }); - } - }, - top: { - my: "bottom", - at: "top", - collision: "fit", - using: function setPositionTop(position, elements) { - const { element, target } = elements; - const calcLeft = target.left - element.left + target.width / 2; - const paddingLeftPosition = calcLeft - 50; - const $jistiPopover = $('.jitsipopover'); - - $jistiPopover.css({ - left: element.left, - top: element.top, - visibility: 'visible' - }); - $jistiPopover.find('.arrow').css({ left: calcLeft }); - $jistiPopover.find('.jitsipopover__menu-padding') - .css({ left: paddingLeftPosition }); - $jistiPopover.find('.jitsipopover__menu-padding-top') - .css({ left: paddingLeftPosition }); - } - } -}; -export default (function () { - /** - * The default options - */ - const defaultOptions = { - skin: 'white', - content: '', - hasArrow: true, - onBeforePosition: undefined, - position: 'top' - }; - - /** - * Constructs new JitsiPopover and attaches it to the element - * @param element jquery selector - * @param options the options for the popover. - * @constructor - */ - function JitsiPopover(element, options) - { - this.options = Object.assign({}, defaultOptions, options); - this.elementIsHovered = false; - this.popoverIsHovered = false; - this.popoverShown = false; - - element.data("jitsi_popover", this); - this.element = element; - this.template = this.getTemplate(); - var self = this; - this.element.on("mouseenter", function () { - self.elementIsHovered = true; - self.show(); - }).on("mouseleave", function () { - self.elementIsHovered = false; - setTimeout(function () { - self.hide(); - }, 10); - }); - } - - /** - * Returns template for popover - */ - JitsiPopover.prototype.getTemplate = function () { - const { hasArrow, position, skin } = this.options; - - let arrow = ''; - if (hasArrow) { - arrow = '
'; - } - - return ( - `
-
- ${arrow} -
-
-
` - ); - }; - - /** - * Shows the popover - */ - JitsiPopover.prototype.show = function () { - if(!JitsiPopover.enabled) - return; - this.createPopover(); - this.popoverShown = true; - }; - - /** - * Hides the popover if not hovered or popover is not shown. - */ - JitsiPopover.prototype.hide = function () { - if(!this.elementIsHovered && !this.popoverIsHovered && - this.popoverShown) { - this.forceHide(); - } - }; - - /** - * Hides the popover and clears the document elements added by popover. - */ - JitsiPopover.prototype.forceHide = function () { - this.remove(); - this.popoverShown = false; - if(this.popoverIsHovered) { //the browser is not firing hover events - //when the element was on hover if got removed. - this.popoverIsHovered = false; - this.onHoverPopover(this.popoverIsHovered); - } - }; - - /** - * Creates the popover html. - */ - JitsiPopover.prototype.createPopover = function () { - let $popover = $('.jitsipopover'); - - if (!$popover.length) { - $('body').append(this.template); - - $popover = $('.jitsipopover'); - - $popover.on('mouseenter', () => { - this.popoverIsHovered = true; - if (typeof this.onHoverPopover === 'function') { - this.onHoverPopover(this.popoverIsHovered); - } - }); - - $popover.on('mouseleave', () => { - this.popoverIsHovered = false; - this.hide(); - if (typeof this.onHoverPopover === 'function') { - this.onHoverPopover(this.popoverIsHovered); - } - }); - } - - const $popoverContent = $popover.find('.jitsipopover__content'); - - /* jshint ignore:start */ - ReactDOM.render( - - { this.options.content } - , - $popoverContent.get(0), - () => { - this.refreshPosition(); - }); - /* jshint ignore:end */ - }; - - /** - * Adds a hover listener to the popover. - */ - JitsiPopover.prototype.addOnHoverPopover = function (listener) { - this.onHoverPopover = listener; - }; - - /** - * Refreshes the position of the popover. - */ - JitsiPopover.prototype.refreshPosition = function () { - const positionOptions = Object.assign( - {}, - positionConfigurations[this.options.position], - { - of: this.element - } - ); - $(".jitsipopover").position(positionOptions); - }; - - /** - * Updates the content of popover. - * @param content new content - */ - JitsiPopover.prototype.updateContent = function (content) { - this.options.content = content; - if (!this.popoverShown) { - return; - } - this.createPopover(); - }; - - /** - * Unmounts any present child React Component and removes the popover itself - * from the DOM. - * - * @returns {void} - */ - JitsiPopover.prototype.remove = function () { - const $popover = $('.jitsipopover'); - const $popoverContent = $popover.find('.jitsipopover__content'); - - if ($popoverContent.length) { - ReactDOM.unmountComponentAtNode($popoverContent.get(0)); - } - - $popover.off(); - $popover.remove(); - }; - - JitsiPopover.enabled = true; - - return JitsiPopover; -})(); diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js index 3b41e79d3..d69a22e21 100644 --- a/modules/UI/videolayout/LocalVideo.js +++ b/modules/UI/videolayout/LocalVideo.js @@ -24,6 +24,9 @@ function LocalVideo(VideoLayout, emitter) { this._buildContextMenu(); this.isLocal = true; this.emitter = emitter; + this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP + ? 'left bottom' : 'top center'; + Object.defineProperty(this, 'id', { get: function () { return APP.conference.getMyUserId(); @@ -67,23 +70,34 @@ LocalVideo.prototype.changeVideo = function (stream) { this.videoStream = stream; let localVideoClick = (event) => { - // TODO Checking the classList is a workround to allow events to bubble + // TODO Checking the classes is a workround to allow events to bubble // into the DisplayName component if it was clicked. React's synthetic // events will fire after jQuery handlers execute, so stop propogation // at this point will prevent DisplayName from getting click events. // This workaround should be removeable once LocalVideo is a React // Component because then the components share the same eventing system. + const $source = $(event.target || event.srcElement); const { classList } = event.target; - const clickedOnDisplayName = classList.contains('displayname') - || classList.contains('editdisplayname'); + + const clickedOnDisplayName + = $source.parents('.displayNameContainer').length > 0; + const clickedOnPopover + = $source.parents('.connection-info').length > 0; + const clickedOnPopoverTrigger + = $source.parents('.popover-trigger').length > 0 + || classList.contains('popover-trigger'); + + const ignoreClick = clickedOnDisplayName + || clickedOnPopoverTrigger + || clickedOnPopover; // FIXME: with Temasys plugin event arg is not an event, but // the clicked object itself, so we have to skip this call - if (event.stopPropagation && !clickedOnDisplayName) { + if (event.stopPropagation && !ignoreClick) { event.stopPropagation(); } - if (!clickedOnDisplayName) { + if (!ignoreClick) { this.VideoLayout.handleVideoThumbClicked(this.id); } }; diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 38804e120..0aec85b8b 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -4,15 +4,14 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; +import { I18nextProvider } from 'react-i18next'; + +import { i18next } from '../../../react/features/base/i18n'; import { PresenceLabel } from '../../../react/features/presence-status'; import { - MuteButton, - KickButton, REMOTE_CONTROL_MENU_STATES, - RemoteControlButton, - RemoteVideoMenu, - VolumeSlider + RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu'; /* eslint-enable no-unused-vars */ @@ -21,13 +20,7 @@ const logger = require("jitsi-meet-logger").getLogger(__filename); import SmallVideo from "./SmallVideo"; import UIUtils from "../util/UIUtil"; -import UIEvents from '../../../service/UI/UIEvents'; -import JitsiPopover from "../util/JitsiPopover"; -const MUTED_DIALOG_BUTTON_VALUES = { - cancel: 0, - muted: 1 -}; const ParticipantConnectionStatus = JitsiMeetJS.constants.participantConnectionStatus; @@ -49,6 +42,8 @@ function RemoteVideo(user, VideoLayout, emitter) { this._audioStreamElement = null; this.hasRemoteVideoMenu = false; this._supportsRemoteControl = false; + this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP + ? 'left top' : 'top center'; this.addRemoteVideoContainer(); this.updateIndicators(); this.setDisplayName(); @@ -78,8 +73,6 @@ function RemoteVideo(user, VideoLayout, emitter) { // Bind event handlers so they are only bound once for every instance. // TODO The event handlers should be turned into actions so changes can be // handled through reducers and middleware. - this._kickHandler = this._kickHandler.bind(this); - this._muteHandler = this._muteHandler.bind(this); this._requestRemoteControlPermissions = this._requestRemoteControlPermissions.bind(this); this._setAudioVolume = this._setAudioVolume.bind(this); @@ -107,39 +100,6 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() { return this.container; }; -/** - * Initializes the remote participant popup menu, by specifying previously - * constructed popupMenuElement, containing all the menu items. - * - * @param popupMenuElement a pre-constructed element, containing the menu items - * to display in the popup - */ -RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) { - let options = { - content: popupMenuElement.outerHTML, - skin: "black", - hasArrow: false, - position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top' - }; - let element = $("#" + this.videoSpanId + " .remotevideomenu"); - this.popover = new JitsiPopover(element, options); - this.popover.addOnHoverPopover(isHovered => { - this.popupMenuIsHovered = isHovered; - this.updateView(); - }); - - // override popover show method to make sure we will update the content - // before showing the popover - let origShowFunc = this.popover.show; - this.popover.show = function () { - // update content by forcing it, to finish even if popover - // is not visible - this.updateRemoteVideoMenu(this.isAudioMuted, true); - // call the original show, passing its actual this - origShowFunc.call(this.popover); - }.bind(this); -}; - /** * Checks whether current video is considered hovered. Currently it is hovered * if the mouse is over the video, or if the connection indicator or the popup @@ -160,6 +120,17 @@ RemoteVideo.prototype._isHovered = function () { * @private */ RemoteVideo.prototype._generatePopupContent = function () { + if (interfaceConfig.filmStripOnly) { + return; + } + + const remoteVideoMenuContainer + = this.container.querySelector('.remotevideomenu'); + + if (!remoteVideoMenuContainer) { + return; + } + const { controller } = APP.remoteControl; let remoteControlState = null; let onRemoteControlToggle; @@ -189,35 +160,28 @@ RemoteVideo.prototype._generatePopupContent = function () { const participantID = this.id; /* jshint ignore:start */ - return ( - - { isModerator - ? + + - : null } - { isModerator - ? - : null } - { remoteControlState - ? - : null } - { onVolumeChange - ? - : null } - - ); + + , + remoteVideoMenuContainer); /* jshint ignore:end */ }; +RemoteVideo.prototype._onRemoteVideoMenuDisplay = function () { + this.updateRemoteVideoMenu(this.isAudioMuted, true); +}; + /** * Sets the remote control supported value and initializes or updates the menu * depending on the remote control is supported or not. @@ -288,27 +252,6 @@ RemoteVideo.prototype._stopRemoteControl = function () { this.updateRemoteVideoMenu(this.isAudioMuted, true); }; -RemoteVideo.prototype._muteHandler = function () { - if (this.isAudioMuted) - return; - - RemoteVideo.showMuteParticipantDialog().then(reason => { - if(reason === MUTED_DIALOG_BUTTON_VALUES.muted) { - this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id); - } - }).catch(e => { - //currently shouldn't be called - logger.error(e); - }); - - this.popover.forceHide(); -}; - -RemoteVideo.prototype._kickHandler = function () { - this.emitter.emit(UIEvents.USER_KICKED, this.id); - this.popover.forceHide(); -}; - /** * Get the remote participant's audio element. * @@ -345,18 +288,10 @@ RemoteVideo.prototype._setAudioVolume = function (newVal) { * @param isMuted the new muted state to update to * @param force to work even if popover is not visible */ -RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) { +RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) { this.isAudioMuted = isMuted; - if (!this.popover) { - return; - } - - // generate content, translate it and add it to document only if - // popover is visible or we force to do so. - if(this.popover.popoverShown || force) { - this.popover.updateContent(this._generatePopupContent()); - } + this._generatePopupContent(); }; /** @@ -395,17 +330,9 @@ RemoteVideo.prototype.addRemoteVideoMenu = function () { if (interfaceConfig.filmStripOnly) { return; } - var spanElement = document.createElement('span'); - spanElement.className = 'remotevideomenu'; - this.container.appendChild(spanElement); + this._generatePopupContent(); - var menuElement = document.createElement('i'); - menuElement.className = 'icon-thumb-menu'; - menuElement.title = 'Remote user controls'; - spanElement.appendChild(menuElement); - - this._initPopupMenu(this._generatePopupContent()); this.hasRemoteVideoMenu = true; }; @@ -538,6 +465,8 @@ RemoteVideo.prototype.remove = function () { this._unmountIndicators(); + this.removeRemoteVideoMenu(); + // Make sure that the large video is updated if are removing its // corresponding small video. this.VideoLayout.updateAfterThumbRemoved(this.id); @@ -591,17 +520,29 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) { // Add click handler. let onClickHandler = (event) => { - let source = event.target || event.srcElement; + const $source = $(event.target || event.srcElement); + const { classList } = event.target; - // ignore click if it was done in popup menu - if ($(source).parents('.popupmenu').length === 0) { + const clickedOnPopover + = $source.parents('.connection-info').length > 0; + const clickedOnPopoverTrigger + = $source.parents('.popover-trigger').length > 0 + || classList.contains('popover-trigger'); + const clickedOnRemoteMenu + = $source.parents('.remotevideomenu').length > 0; + + const ignoreClick = clickedOnPopoverTrigger + || clickedOnPopover + || clickedOnRemoteMenu; + + if (!ignoreClick) { this.VideoLayout.handleVideoThumbClicked(this.id); } // On IE we need to populate this handler on video // and it does not give event instance as an argument, // so we check here for methods. - if (event.stopPropagation && event.preventDefault) { + if (event.stopPropagation && event.preventDefault && !ignoreClick) { event.stopPropagation(); event.preventDefault(); } @@ -665,8 +606,9 @@ RemoteVideo.prototype.setDisplayName = function(displayName) { */ RemoteVideo.prototype.removeRemoteVideoMenu = function() { var menuSpan = $('#' + this.videoSpanId + '> .remotevideomenu'); + if (menuSpan.length) { - this.popover.forceHide(); + ReactDOM.unmountComponentAtNode(menuSpan.get(0)); menuSpan.remove(); this.hasRemoteVideoMenu = false; } @@ -740,30 +682,12 @@ RemoteVideo.createContainer = function (spanId) { presenceLabelContainer.className = 'presence-label-container'; container.appendChild(presenceLabelContainer); + const remoteVideoMenuContainer = document.createElement('span'); + remoteVideoMenuContainer.className = 'remotevideomenu'; + container.appendChild(remoteVideoMenuContainer); + var remotes = document.getElementById('filmstripRemoteVideosContainer'); return remotes.appendChild(container); }; -/** - * Shows 2 button dialog for confirmation from the user for muting remote - * participant. - */ -RemoteVideo.showMuteParticipantDialog = function () { - return new Promise(resolve => { - APP.UI.messageHandler.openTwoButtonDialog({ - titleKey : "dialog.muteParticipantTitle", - msgString: "
", - leftButtonKey: "dialog.muteParticipantButton", - dontShowAgain: { - id: "dontShowMuteParticipantDialog", - textKey: "dialog.doNotShowMessageAgain", - checked: true, - buttonValues: [true] - }, - submitFunction: () => resolve(MUTED_DIALOG_BUTTON_VALUES.muted), - closeFunction: () => resolve(MUTED_DIALOG_BUTTON_VALUES.cancel) - }); - }); -}; - export default RemoteVideo; diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 6f9853839..a6ca49db3 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -747,21 +747,24 @@ SmallVideo.prototype.updateIndicators = function () { /* jshint ignore:start */ ReactDOM.render( -
- { this._showConnectionIndicator - ? - : null } - { this._showRaisedHand - ? : null } - { this._showDominantSpeaker - ? : null } -
, + +
+ { this._showConnectionIndicator + ? + : null } + { this._showRaisedHand + ? + : null } + { this._showDominantSpeaker + ? + : null } +
+
, indicatorToolbar ); /* jshint ignore:end */ diff --git a/react/features/base/participants/actionTypes.js b/react/features/base/participants/actionTypes.js index 83f4bbc66..189fb0f37 100644 --- a/react/features/base/participants/actionTypes.js +++ b/react/features/base/participants/actionTypes.js @@ -10,6 +10,26 @@ */ export const DOMINANT_SPEAKER_CHANGED = Symbol('DOMINANT_SPEAKER_CHANGED'); +/** + * Create an action for removing a participant from the conference. + * + * { + * type: KICK_PARTICIPANT, + * id: string + * } + */ +export const KICK_PARTICIPANT = Symbol('KICK_PARTICIPANT'); + +/** + * Create an action for muting a remote participant. + * + * { + * type: MUTE_REMOTE_PARTICIPANT, + * id: string + * } + */ +export const MUTE_REMOTE_PARTICIPANT = Symbol('MUTE_REMOTE_PARTICIPANT'); + /** * Create an action for when the local participant's display name is updated. * diff --git a/react/features/base/participants/actions.js b/react/features/base/participants/actions.js index 50185ac0e..f3cf22ac6 100644 --- a/react/features/base/participants/actions.js +++ b/react/features/base/participants/actions.js @@ -1,5 +1,7 @@ import { DOMINANT_SPEAKER_CHANGED, + KICK_PARTICIPANT, + MUTE_REMOTE_PARTICIPANT, PARTICIPANT_DISPLAY_NAME_CHANGED, PARTICIPANT_ID_CHANGED, PARTICIPANT_JOINED, @@ -50,6 +52,22 @@ export function localParticipantConnectionStatusChanged(connectionStatus) { }; } +/** + * Create an action for removing a participant from the conference. + * + * @param {string} id - Participant's ID. + * @returns {{ + * type: KICK_PARTICIPANT, + * id: string + * }} + */ +export function kickParticipant(id) { + return { + type: KICK_PARTICIPANT, + id + }; +} + /** * Action to signal that the ID of local participant has changed. It happens * when the local participant joins a new conference or leaves an existing @@ -106,6 +124,22 @@ export function localParticipantRoleChanged(role) { }; } +/** + * Create an action for muting another participant in the conference. + * + * @param {string} id - Participant's ID. + * @returns {{ + * type: MUTE_REMOTE_PARTICIPANT, + * id: string + * }} + */ +export function muteRemoteParticipant(id) { + return { + type: MUTE_REMOTE_PARTICIPANT, + id + }; +} + /** * Action to update a participant's connection status. * diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index 6598a53e5..af6f59fca 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -7,7 +7,11 @@ import { import { MiddlewareRegistry } from '../redux'; import { localParticipantIdChanged } from './actions'; -import { PARTICIPANT_DISPLAY_NAME_CHANGED } from './actionTypes'; +import { + KICK_PARTICIPANT, + MUTE_REMOTE_PARTICIPANT, + PARTICIPANT_DISPLAY_NAME_CHANGED +} from './actionTypes'; import { LOCAL_PARTICIPANT_DEFAULT_ID } from './constants'; import { getLocalParticipant } from './functions'; @@ -30,6 +34,32 @@ MiddlewareRegistry.register(store => next => action => { store.dispatch(localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID)); break; + case KICK_PARTICIPANT: + if (typeof APP !== 'undefined') { + APP.UI.emitEvent(UIEvents.USER_KICKED, action.id); + } + break; + + case MUTE_REMOTE_PARTICIPANT: + if (typeof APP !== 'undefined') { + APP.UI.messageHandler.openTwoButtonDialog({ + titleKey: 'dialog.muteParticipantTitle', + msgString: + '
', + leftButtonKey: 'dialog.muteParticipantButton', + dontShowAgain: { + id: 'dontShowMuteParticipantDialog', + textKey: 'dialog.doNotShowMessageAgain', + checked: true, + buttonValues: [ true ] + }, + submitFunction: () => { + APP.UI.emitEvent(UIEvents.REMOTE_AUDIO_MUTED, action.id); + } + }); + } + break; + // TODO Remove this middleware when the local display name update flow is // fully brought into redux. case PARTICIPANT_DISPLAY_NAME_CHANGED: { diff --git a/react/features/connection-indicator/components/ConnectionIndicator.js b/react/features/connection-indicator/components/ConnectionIndicator.js index f67288d95..bae11c0d6 100644 --- a/react/features/connection-indicator/components/ConnectionIndicator.js +++ b/react/features/connection-indicator/components/ConnectionIndicator.js @@ -1,7 +1,6 @@ +import AKInlineDialog from '@atlaskit/inline-dialog'; import React, { Component } from 'react'; -import JitsiPopover from '../../../../modules/UI/util/JitsiPopover'; - import { JitsiParticipantConnectionStatus } from '../../base/lib-jitsi-meet'; import { ConnectionStatsTable } from '../../connection-stats'; @@ -67,21 +66,22 @@ class ConnectionIndicator extends Component { */ connectionStatus: React.PropTypes.string, + /** + * Whether or not clicking the indicator should display a popover for + * more details. + */ + enableStatsDisplay: React.PropTypes.bool, + /** * Whether or not the displays stats are for local video. */ isLocalVideo: React.PropTypes.bool, /** - * The callback to invoke when the hover state over the popover changes. + * Relative to the icon from where the popover for more connection + * details should display. */ - onHover: React.PropTypes.func, - - /** - * Whether or not the popover should display a link that can toggle - * a more detailed view of the stats. - */ - showMoreLink: React.PropTypes.bool, + statsPopoverPosition: React.PropTypes.string, /** * Invoked to obtain translated strings. @@ -104,16 +104,6 @@ class ConnectionIndicator extends Component { constructor(props) { super(props); - /** - * The internal reference to topmost DOM/HTML element backing the React - * {@code Component}. Accessed directly for associating an element as - * the trigger for a popover. - * - * @private - * @type {HTMLDivElement} - */ - this._rootElement = null; - this.state = { /** * Whether or not the popover content should display additional @@ -134,12 +124,14 @@ class ConnectionIndicator extends Component { // Bind event handlers so they are only bound once for every instance. this._onStatsUpdated = this._onStatsUpdated.bind(this); + this._onStatsClose = this._onStatsClose.bind(this); + this._onStatsToggle = this._onStatsToggle.bind(this); + this._onStatsUpdated = this._onStatsUpdated.bind(this); this._onToggleShowMore = this._onToggleShowMore.bind(this); - this._setRootElement = this._setRootElement.bind(this); } /** - * Creates a popover instance to display when the component is hovered. + * Starts listening for stat updates. * * @inheritdoc * returns {void} @@ -147,20 +139,10 @@ class ConnectionIndicator extends Component { componentDidMount() { statsEmitter.subscribeToClientStats( this.props.userID, this._onStatsUpdated); - - this.popover = new JitsiPopover($(this._rootElement), { - content: this._renderStatisticsTable(), - skin: 'black', - position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top' - }); - - this.popover.addOnHoverPopover(this.props.onHover); } /** - * Updates the contents of the popover. This is done manually because the - * popover is not a React Component yet and so is not automatiucally aware - * of changed data. + * Updates which user's stats are being listened to. * * @inheritdoc * returns {void} @@ -172,22 +154,17 @@ class ConnectionIndicator extends Component { statsEmitter.subscribeToClientStats( this.props.userID, this._onStatsUpdated); } - - this.popover.updateContent(this._renderStatisticsTable()); } /** - * Cleans up any popover instance that is linked to the component. + * Sets the state to hide the Statistics Table popover. * - * @inheritdoc - * returns {void} + * @private + * @returns {void} */ componentWillUnmount() { statsEmitter.unsubscribeToClientStats( this.props.userID, this._onStatsUpdated); - - this.popover.forceHide(); - this.popover.remove(); } /** @@ -198,16 +175,48 @@ class ConnectionIndicator extends Component { */ render() { return ( -
-
- { this._renderIcon() } -
+
+ +
+
+
+ { this._renderIcon() } +
+
+
+
); } + /** + * Sets the state not to show the Statistics Table popover. + * + * @private + * @returns {void} + */ + _onStatsClose() { + this.setState({ showStats: false }); + } + + /** + * Sets the state to show or hide the Statistics Table popover. + * + * @private + * @returns {void} + */ + _onStatsToggle() { + if (this.props.enableStatsDisplay) { + this.setState({ showStats: !this.state.showStats }); + } + } + /** * Callback invoked when new connection stats associated with the passed in * user ID are available. Will update the component's display of current @@ -314,17 +323,6 @@ class ConnectionIndicator extends Component { transport = { transport } /> ); } - - /** - * Sets an internal reference to the component's root element. - * - * @param {Object} element - The highest DOM element in the component. - * @private - * @returns {void} - */ - _setRootElement(element) { - this._rootElement = element; - } } export default ConnectionIndicator; diff --git a/react/features/connection-stats/components/ConnectionStatsTable.js b/react/features/connection-stats/components/ConnectionStatsTable.js index ccc1c842d..2de2ed66f 100644 --- a/react/features/connection-stats/components/ConnectionStatsTable.js +++ b/react/features/connection-stats/components/ConnectionStatsTable.js @@ -292,7 +292,7 @@ class ConnectionStatsTable extends Component { return ( { this.props.t(translationKey) } diff --git a/react/features/filmstrip/components/web/BaseIndicator.js b/react/features/filmstrip/components/web/BaseIndicator.js index 56e7392a7..c61610320 100644 --- a/react/features/filmstrip/components/web/BaseIndicator.js +++ b/react/features/filmstrip/components/web/BaseIndicator.js @@ -27,7 +27,7 @@ class BaseIndicator extends Component { iconClassName: React.PropTypes.string, /** - * The front size for the icon. + * The font size for the icon. */ iconSize: React.PropTypes.string, diff --git a/react/features/remote-video-menu/components/KickButton.js b/react/features/remote-video-menu/components/KickButton.js index 7504c6a56..64e04642b 100644 --- a/react/features/remote-video-menu/components/KickButton.js +++ b/react/features/remote-video-menu/components/KickButton.js @@ -1,6 +1,8 @@ import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { translate } from '../../base/i18n'; +import { kickParticipant } from '../../base/participants'; import RemoteVideoMenuButton from './RemoteVideoMenuButton'; @@ -18,7 +20,13 @@ class KickButton extends Component { */ static propTypes = { /** - * The callback to invoke when the component is clicked. + * Invoked to signal the participant with the passed in participantID + * should be removed from the conference. + */ + dispatch: React.PropTypes.func, + + /** + * Callback to invoke when {@code KickButton} is clicked. */ onClick: React.PropTypes.func, @@ -33,6 +41,19 @@ class KickButton extends Component { t: React.PropTypes.func }; + /** + * Initializes a new {@code KickButton} 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._onClick = this._onClick.bind(this); + } + /** * Implements React's {@link Component#render()}. * @@ -40,16 +61,32 @@ class KickButton extends Component { * @returns {ReactElement} */ render() { - const { onClick, participantID, t } = this.props; + const { participantID, t } = this.props; return ( + onClick = { this._onClick } /> ); } + + /** + * Remove the participant with associated participantID from the conference. + * + * @private + * @returns {void} + */ + _onClick() { + const { dispatch, onClick, participantID } = this.props; + + dispatch(kickParticipant(participantID)); + + if (onClick) { + onClick(); + } + } } -export default translate(KickButton); +export default translate(connect()(KickButton)); diff --git a/react/features/remote-video-menu/components/MuteButton.js b/react/features/remote-video-menu/components/MuteButton.js index 2f59e3040..fbc340aac 100644 --- a/react/features/remote-video-menu/components/MuteButton.js +++ b/react/features/remote-video-menu/components/MuteButton.js @@ -1,6 +1,8 @@ import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { translate } from '../../base/i18n'; +import { muteRemoteParticipant } from '../../base/participants'; import RemoteVideoMenuButton from './RemoteVideoMenuButton'; @@ -17,13 +19,19 @@ class MuteButton extends Component { * @static */ static propTypes = { + /** + * Invoked to send a request for muting the participant with the passed + * in participantID. + */ + dispatch: React.PropTypes.func, + /** * Whether or not the participant is currently audio muted. */ isAudioMuted: React.PropTypes.bool, /** - * The callback to invoke when the component is clicked. + * Callback to invoke when {@code MuteButton} is clicked. */ onClick: React.PropTypes.func, @@ -38,6 +46,19 @@ class MuteButton extends Component { t: React.PropTypes.func }; + /** + * Initializes a new {@code MuteButton} 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._onClick = this._onClick.bind(this); + } + /** * Implements React's {@link Component#render()}. * @@ -45,7 +66,7 @@ class MuteButton extends Component { * @returns {ReactElement} */ render() { - const { isAudioMuted, onClick, participantID, t } = this.props; + const { isAudioMuted, participantID, t } = this.props; const muteConfig = isAudioMuted ? { translationKey: 'videothumbnail.muted', muteClassName: 'mutelink disabled' @@ -60,9 +81,26 @@ class MuteButton extends Component { displayClass = { muteConfig.muteClassName } iconClass = 'icon-mic-disabled' id = { `mutelink_${participantID}` } - onClick = { onClick } /> + onClick = { this._onClick } /> ); } + + /** + * Dispatches a request to mute the participant with the passed in + * participantID. + * + * @private + * @returns {void} + */ + _onClick() { + const { dispatch, onClick, participantID } = this.props; + + dispatch(muteRemoteParticipant(participantID)); + + if (onClick) { + onClick(); + } + } } -export default translate(MuteButton); +export default translate(connect()(MuteButton)); diff --git a/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js b/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js new file mode 100644 index 000000000..99f11b593 --- /dev/null +++ b/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js @@ -0,0 +1,194 @@ +import AKInlineDialog from '@atlaskit/inline-dialog'; +import React, { Component } from 'react'; + +import { + MuteButton, + KickButton, + RemoteControlButton, + RemoteVideoMenu, + VolumeSlider +} from './'; + +declare var $: Object; +declare var interfaceConfig: Object; + +/** + * React {@code Component} for displaying an icon associated with opening the + * the {@code RemoteVideoMenu}. + * + * @extends {Component} + */ +class RemoteVideoMenuTriggerButton extends Component { + static propTypes = { + /** + * A value between 0 and 1 indicating the volume of the participant's + * audio element. + */ + initialVolumeValue: React.PropTypes.number, + + /** + * Whether or not the participant is currently muted. + */ + isAudioMuted: React.PropTypes.bool, + + /** + * Whether or not the participant is a conference moderator. + */ + isModerator: React.PropTypes.bool, + + /** + * Callback to invoke when the popover has been displayed. + */ + onMenuDisplay: React.PropTypes.func, + + /** + * Callback to invoke choosing to start a remote control session with + * the participant. + */ + onRemoteControlToggle: React.PropTypes.func, + + /** + * Callback to invoke when changing the level of the participant's + * audio element. + */ + onVolumeChange: React.PropTypes.func, + + /** + * The ID for the participant on which the remote video menu will act. + */ + participantID: React.PropTypes.string, + + /** + * The current state of the participant's remote control session. + */ + remoteControlState: React.PropTypes.number + }; + + /** + * Initializes a new {#@code RemoteVideoMenuTriggerButton} instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props) { + super(props); + + this.state = { + showRemoteMenu: false + }; + + /** + * The internal reference to topmost DOM/HTML element backing the React + * {@code Component}. Accessed directly for associating an element as + * the trigger for a popover. + * + * @private + * @type {HTMLDivElement} + */ + this._rootElement = null; + + // Bind event handlers so they are only bound once for every instance. + this._onRemoteMenuClose = this._onRemoteMenuClose.bind(this); + this._onRemoteMenuToggle = this._onRemoteMenuToggle.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + return ( + + + + + + ); + } + + /** + * Closes the {@code RemoteVideoMenu}. + * + * @private + * @returns {void} + */ + _onRemoteMenuClose() { + this.setState({ showRemoteMenu: false }); + } + + /** + * Opens or closes the {@code RemoteVideoMenu}. + * + * @private + * @returns {void} + */ + _onRemoteMenuToggle() { + const willShowRemoteMenu = !this.state.showRemoteMenu; + + if (willShowRemoteMenu) { + this.props.onMenuDisplay(); + } + + this.setState({ showRemoteMenu: willShowRemoteMenu }); + } + + /** + * Creates a new {@code RemoteVideoMenu} with buttons for interacting with + * the remote participant. + * + * @private + * @returns {ReactElement} + */ + _renderRemoteVideoMenu() { + const { + initialVolumeValue, + isAudioMuted, + isModerator, + onRemoteControlToggle, + onVolumeChange, + remoteControlState, + participantID + } = this.props; + + return ( + + { isModerator + ? + : null } + { isModerator + ? + : null } + { remoteControlState + ? + : null } + { onVolumeChange + ? + : null } + + ); + } +} + +export default RemoteVideoMenuTriggerButton; diff --git a/react/features/remote-video-menu/components/index.js b/react/features/remote-video-menu/components/index.js index 214fda9e8..b8daab3b9 100644 --- a/react/features/remote-video-menu/components/index.js +++ b/react/features/remote-video-menu/components/index.js @@ -5,4 +5,7 @@ export { default as RemoteControlButton } from './RemoteControlButton'; export { default as RemoteVideoMenu } from './RemoteVideoMenu'; +export { + default as RemoteVideoMenuTriggerButton +} from './RemoteVideoMenuTriggerButton'; export { default as VolumeSlider } from './VolumeSlider';