feat(jitsipopover): convert to InlineDialog (#1804)
* feat(small-video): use InlineDialog for stats and remote menu - Remove JitsiPopover and use InlineDialog instead. - Bring the remote menu icon into react. - Make vertical filmstrip position:fixed so popper (AtlasKit dependency) sets InlineDialogs and eventually tooltips to position:fixed. * ref(remote-menu): hook KickButton to redux * ref(remote-menu): hook MuteButton to redux * modify padding, toggle dialogs * pixel push margins to align dialogs, adjust padding of dialogs * add comment about margin for dialog, add file I forgot * modify indicator markup so the icon can be moved down while trigger stays at top of toolbar
This commit is contained in:
parent
cd910e3074
commit
725d39ddcd
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -112,7 +112,6 @@ $tooltipsZ: 401;
|
|||
$dropdownMaskZ: 900;
|
||||
$dropdownZ: 901;
|
||||
$centeredVideoLabelZ: 1010;
|
||||
$jitsipopoverZ: 1012;
|
||||
$popoverZ: 1015;
|
||||
$overlayZ: 1016;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -50,7 +50,6 @@
|
|||
@import 'recording';
|
||||
@import 'login_menu';
|
||||
@import 'popover';
|
||||
@import 'jitsi_popover';
|
||||
@import 'contact_list';
|
||||
@import 'chat';
|
||||
@import 'ringing/ringing';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = '<div class="arrow"></div>';
|
||||
}
|
||||
|
||||
return (
|
||||
`<div class="jitsipopover ${skin} ${position}">
|
||||
<div class="jitsipopover__menu-padding-top"></div>
|
||||
${arrow}
|
||||
<div class="jitsipopover__content"></div>
|
||||
<div class="jitsipopover__menu-padding"></div>
|
||||
</div>`
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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(
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
{ this.options.content }
|
||||
</I18nextProvider>,
|
||||
$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;
|
||||
})();
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<RemoteVideoMenu id = { participantID }>
|
||||
{ isModerator
|
||||
? <MuteButton
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<RemoteVideoMenuTriggerButton
|
||||
initialVolumeValue = { initialVolumeValue }
|
||||
isAudioMuted = { this.isAudioMuted }
|
||||
onClick = { this._muteHandler }
|
||||
participantID = { participantID } />
|
||||
: null }
|
||||
{ isModerator
|
||||
? <KickButton
|
||||
onClick = { this._kickHandler }
|
||||
participantID = { participantID } />
|
||||
: null }
|
||||
{ remoteControlState
|
||||
? <RemoteControlButton
|
||||
onClick = { onRemoteControlToggle }
|
||||
isModerator = { isModerator }
|
||||
onMenuDisplay = { this._onRemoteVideoMenuDisplay.bind(this) }
|
||||
onRemoteControlToggle = { onRemoteControlToggle }
|
||||
onVolumeChange = { onVolumeChange }
|
||||
participantID = { participantID }
|
||||
remoteControlState = { remoteControlState } />
|
||||
: null }
|
||||
{ onVolumeChange
|
||||
? <VolumeSlider
|
||||
initialValue = { initialVolumeValue }
|
||||
onChange = { onVolumeChange } />
|
||||
: null }
|
||||
</RemoteVideoMenu>
|
||||
);
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
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 <object>
|
||||
// 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: "<div data-i18n='dialog.muteParticipantBody'></div>",
|
||||
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;
|
||||
|
|
|
@ -747,21 +747,24 @@ SmallVideo.prototype.updateIndicators = function () {
|
|||
|
||||
/* jshint ignore:start */
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
{ this._showConnectionIndicator
|
||||
? <ConnectionIndicator
|
||||
connectionStatus = { this._connectionStatus }
|
||||
iconSize = { iconSize }
|
||||
isLocalVideo = { this.isLocal }
|
||||
onHover = { this._onPopoverHover }
|
||||
showMoreLink = { this.isLocal }
|
||||
userID = { this.id } />
|
||||
: null }
|
||||
{ this._showRaisedHand
|
||||
? <RaisedHandIndicator iconSize = { iconSize } /> : null }
|
||||
{ this._showDominantSpeaker
|
||||
? <DominantSpeakerIndicator iconSize = { iconSize } /> : null }
|
||||
</div>,
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<div>
|
||||
{ this._showConnectionIndicator
|
||||
? <ConnectionIndicator
|
||||
connectionStatus = { this._connectionStatus }
|
||||
isLocalVideo = { this.isLocal }
|
||||
enableStatsDisplay = { !interfaceConfig.filmStripOnly }
|
||||
statsPopoverPosition = { this.statsPopoverLocation }
|
||||
userID = { this.id } />
|
||||
: null }
|
||||
{ this._showRaisedHand
|
||||
? <RaisedHandIndicator iconSize = { iconSize } />
|
||||
: null }
|
||||
{ this._showDominantSpeaker
|
||||
? <DominantSpeakerIndicator iconSize = { iconSize } />
|
||||
: null }
|
||||
</div>
|
||||
</I18nextProvider>,
|
||||
indicatorToolbar
|
||||
);
|
||||
/* jshint ignore:end */
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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:
|
||||
'<div data-i18n="dialog.muteParticipantBody"></div>',
|
||||
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: {
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
className = 'connection-indicator indicator'
|
||||
ref = { this._setRootElement }>
|
||||
<div className = 'connection indicatoricon'>
|
||||
{ this._renderIcon() }
|
||||
</div>
|
||||
<div className = 'connection-indicator-container'>
|
||||
<AKInlineDialog
|
||||
content = { this._renderStatisticsTable() }
|
||||
isOpen = { this.state.showStats }
|
||||
onClose = { this._onStatsClose }
|
||||
position = { this.props.statsPopoverPosition }>
|
||||
<div
|
||||
className = 'popover-trigger'
|
||||
onClick = { this._onStatsToggle }>
|
||||
<div className = 'connection-indicator indicator'>
|
||||
<div className = 'connection indicatoricon'>
|
||||
{ this._renderIcon() }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AKInlineDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
|
|
@ -292,7 +292,7 @@ class ConnectionStatsTable extends Component {
|
|||
|
||||
return (
|
||||
<a
|
||||
className = 'jitsipopover__showmore link'
|
||||
className = 'showmore link'
|
||||
onClick = { this.props.onShowMore } >
|
||||
{ this.props.t(translationKey) }
|
||||
</a>
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<RemoteVideoMenuButton
|
||||
buttonText = { t('videothumbnail.kick') }
|
||||
iconClass = 'icon-kick'
|
||||
id = { `ejectlink_${participantID}` }
|
||||
onClick = { onClick } />
|
||||
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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 (
|
||||
<AKInlineDialog
|
||||
content = { this._renderRemoteVideoMenu() }
|
||||
isOpen = { this.state.showRemoteMenu }
|
||||
onClose = { this._onRemoteMenuClose }
|
||||
position = { interfaceConfig.VERTICAL_FILMSTRIP
|
||||
? 'left middle' : 'top center' }
|
||||
shouldFlip = { true }>
|
||||
<span
|
||||
className = 'popover-trigger remote-video-menu-trigger'
|
||||
onClick = { this._onRemoteMenuToggle }>
|
||||
<i
|
||||
className = 'icon-thumb-menu'
|
||||
title = 'Remote user controls' />
|
||||
</span>
|
||||
</AKInlineDialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<RemoteVideoMenu id = { participantID }>
|
||||
{ isModerator
|
||||
? <MuteButton
|
||||
isAudioMuted = { isAudioMuted }
|
||||
onClick = { this._onRemoteMenuClose }
|
||||
participantID = { participantID } />
|
||||
: null }
|
||||
{ isModerator
|
||||
? <KickButton
|
||||
onClick = { this._onRemoteMenuClose }
|
||||
participantID = { participantID } />
|
||||
: null }
|
||||
{ remoteControlState
|
||||
? <RemoteControlButton
|
||||
onClick = { onRemoteControlToggle }
|
||||
participantID = { participantID }
|
||||
remoteControlState = { remoteControlState } />
|
||||
: null }
|
||||
{ onVolumeChange
|
||||
? <VolumeSlider
|
||||
initialValue = { initialVolumeValue }
|
||||
onChange = { onVolumeChange } />
|
||||
: null }
|
||||
</RemoteVideoMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RemoteVideoMenuTriggerButton;
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue