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 {
|
%connection-info {
|
||||||
text-align: left;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: $popoverFontColor;
|
color: $modalTextColor;
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
|
@ -11,11 +10,14 @@
|
||||||
|
|
||||||
.connection-info
|
.connection-info
|
||||||
{
|
{
|
||||||
float: left;
|
|
||||||
padding: 5px;
|
|
||||||
padding-left: 0;
|
|
||||||
@extend %connection-info;
|
@extend %connection-info;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply negative margin to reduce the appearance of padding in AtlasKit
|
||||||
|
* InlineDialog.
|
||||||
|
*/
|
||||||
|
margin: -15px;
|
||||||
|
|
||||||
> table {
|
> table {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@extend %connection-info;
|
@extend %connection-info;
|
||||||
|
@ -40,4 +42,11 @@
|
||||||
@extend .connection-info__icon;
|
@extend .connection-info__icon;
|
||||||
color: $uploadConnectionIconColor;
|
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 {
|
.popupmenu {
|
||||||
|
text-align: left;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 2px 0;
|
white-space: nowrap;
|
||||||
bottom: 0;
|
|
||||||
height: auto;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
text-align: left;
|
|
||||||
height: 35px;
|
height: 35px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $popupMenuSelectedItemBackground;
|
background-color: rgba(9, 30, 66, 0.04);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link Appearance
|
// Link Appearance
|
||||||
&__link,
|
&__link,
|
||||||
&__contents {
|
&__contents {
|
||||||
|
color: $modalTextColor;
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #fff;
|
|
||||||
padding: 5px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: 9pt;
|
font-size: 9pt;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: hand;
|
cursor: pointer;
|
||||||
|
padding: 0 5px;
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
color: gray !important;
|
color: gray !important;
|
||||||
|
@ -46,6 +40,12 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__link {
|
||||||
|
i {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__contents {
|
&__contents {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
@ -73,7 +73,6 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
@include absoluteAligning();
|
@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 {
|
span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
|
||||||
display:block !important;
|
display:block !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,6 @@ $tooltipsZ: 401;
|
||||||
$dropdownMaskZ: 900;
|
$dropdownMaskZ: 900;
|
||||||
$dropdownZ: 901;
|
$dropdownZ: 901;
|
||||||
$centeredVideoLabelZ: 1010;
|
$centeredVideoLabelZ: 1010;
|
||||||
$jitsipopoverZ: 1012;
|
|
||||||
$popoverZ: 1015;
|
$popoverZ: 1015;
|
||||||
$overlayZ: 1016;
|
$overlayZ: 1016;
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,14 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
height: 100%;
|
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.
|
* 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
|
* Move the remote video menu trigger to the bottom left of the
|
||||||
* video thumbnail.
|
* video thumbnail.
|
||||||
*/
|
*/
|
||||||
.remotevideomenu {
|
.remotevideomenu,
|
||||||
|
.remote-video-menu-trigger {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: auto;
|
top: auto;
|
||||||
right: auto;
|
right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-video-menu-trigger {
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
transform: translate3d(0,0,0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#remoteVideos {
|
#remoteVideos {
|
||||||
|
@ -75,11 +86,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.videocontainer {
|
.videocontainer {
|
||||||
&__toolbar,
|
|
||||||
&__toptoolbar {
|
|
||||||
transform: translate3d(0,0,0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move status icons to the bottom right of the thumbnail.
|
* Move status icons to the bottom right of the thumbnail.
|
||||||
*/
|
*/
|
||||||
|
@ -159,4 +165,17 @@
|
||||||
transition-delay: 0.1s;
|
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 {
|
&__toolbar {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: 0 5px 0 5px;
|
|
||||||
height: $thumbnailToolbarHeight;
|
height: $thumbnailToolbarHeight;
|
||||||
|
padding: 0 5px 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__toptoolbar {
|
&__toptoolbar {
|
||||||
$toolbarPadding: 5px;
|
$toolbarIconMargin: 5px;
|
||||||
top: 0;
|
top: 0;
|
||||||
padding: $toolbarPadding;
|
|
||||||
padding-bottom: 0;
|
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,
|
.connection-indicator,
|
||||||
span.indicator {
|
span.indicator {
|
||||||
|
@ -71,6 +86,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connection-indicator-container {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
.popover-trigger {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.connection-indicator,
|
.connection-indicator,
|
||||||
span.indicator {
|
span.indicator {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -78,7 +102,6 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: $thumbnailIndicatorSize;
|
line-height: $thumbnailIndicatorSize;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
float: left;
|
|
||||||
@include circle($thumbnailIndicatorSize);
|
@include circle($thumbnailIndicatorSize);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
z-index: $zindex3;
|
z-index: $zindex3;
|
||||||
|
@ -124,6 +147,7 @@
|
||||||
|
|
||||||
.icon-connection,
|
.icon-connection,
|
||||||
.icon-connection-lost {
|
.icon-connection-lost {
|
||||||
|
cursor: pointer;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,13 +333,13 @@
|
||||||
background: $connectionIndicatorBg;
|
background: $connectionIndicatorBg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remote-video-menu-trigger,
|
||||||
.remotevideomenu
|
.remotevideomenu
|
||||||
{
|
{
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin-top: 7px;
|
|
||||||
z-index: $zindex3;
|
z-index: $zindex3;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 13px;
|
height: 13px;
|
||||||
|
@ -326,6 +350,9 @@
|
||||||
cursor: hand;
|
cursor: hand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.remote-video-menu-trigger {
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio indicator on video thumbnails.
|
* Audio indicator on video thumbnails.
|
||||||
|
|
|
@ -50,7 +50,6 @@
|
||||||
@import 'recording';
|
@import 'recording';
|
||||||
@import 'login_menu';
|
@import 'login_menu';
|
||||||
@import 'popover';
|
@import 'popover';
|
||||||
@import 'jitsi_popover';
|
|
||||||
@import 'contact_list';
|
@import 'contact_list';
|
||||||
@import 'chat';
|
@import 'chat';
|
||||||
@import 'ringing/ringing';
|
@import 'ringing/ringing';
|
||||||
|
|
|
@ -8,7 +8,6 @@ import Chat from "./side_pannels/chat/Chat";
|
||||||
import SidePanels from "./side_pannels/SidePanels";
|
import SidePanels from "./side_pannels/SidePanels";
|
||||||
import Avatar from "./avatar/Avatar";
|
import Avatar from "./avatar/Avatar";
|
||||||
import SideContainerToggler from "./side_pannels/SideContainerToggler";
|
import SideContainerToggler from "./side_pannels/SideContainerToggler";
|
||||||
import JitsiPopover from "./util/JitsiPopover";
|
|
||||||
import messageHandler from "./util/MessageHandler";
|
import messageHandler from "./util/MessageHandler";
|
||||||
import UIUtil from "./util/UIUtil";
|
import UIUtil from "./util/UIUtil";
|
||||||
import { activateTooltips } from './util/Tooltip';
|
import { activateTooltips } from './util/Tooltip';
|
||||||
|
@ -322,7 +321,6 @@ UI.start = function () {
|
||||||
UI.showToolbar();
|
UI.showToolbar();
|
||||||
Filmstrip.setFilmstripOnly();
|
Filmstrip.setFilmstripOnly();
|
||||||
APP.store.dispatch(setNotificationsEnabled(false));
|
APP.store.dispatch(setNotificationsEnabled(false));
|
||||||
JitsiPopover.enabled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
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._buildContextMenu();
|
||||||
this.isLocal = true;
|
this.isLocal = true;
|
||||||
this.emitter = emitter;
|
this.emitter = emitter;
|
||||||
|
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
|
||||||
|
? 'left bottom' : 'top center';
|
||||||
|
|
||||||
Object.defineProperty(this, 'id', {
|
Object.defineProperty(this, 'id', {
|
||||||
get: function () {
|
get: function () {
|
||||||
return APP.conference.getMyUserId();
|
return APP.conference.getMyUserId();
|
||||||
|
@ -67,23 +70,34 @@ LocalVideo.prototype.changeVideo = function (stream) {
|
||||||
this.videoStream = stream;
|
this.videoStream = stream;
|
||||||
|
|
||||||
let localVideoClick = (event) => {
|
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
|
// into the DisplayName component if it was clicked. React's synthetic
|
||||||
// events will fire after jQuery handlers execute, so stop propogation
|
// events will fire after jQuery handlers execute, so stop propogation
|
||||||
// at this point will prevent DisplayName from getting click events.
|
// at this point will prevent DisplayName from getting click events.
|
||||||
// This workaround should be removeable once LocalVideo is a React
|
// This workaround should be removeable once LocalVideo is a React
|
||||||
// Component because then the components share the same eventing system.
|
// Component because then the components share the same eventing system.
|
||||||
|
const $source = $(event.target || event.srcElement);
|
||||||
const { classList } = event.target;
|
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
|
// FIXME: with Temasys plugin event arg is not an event, but
|
||||||
// the clicked object itself, so we have to skip this call
|
// the clicked object itself, so we have to skip this call
|
||||||
if (event.stopPropagation && !clickedOnDisplayName) {
|
if (event.stopPropagation && !ignoreClick) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clickedOnDisplayName) {
|
if (!ignoreClick) {
|
||||||
this.VideoLayout.handleVideoThumbClicked(this.id);
|
this.VideoLayout.handleVideoThumbClicked(this.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,15 +4,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
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 { PresenceLabel } from '../../../react/features/presence-status';
|
||||||
import {
|
import {
|
||||||
MuteButton,
|
|
||||||
KickButton,
|
|
||||||
REMOTE_CONTROL_MENU_STATES,
|
REMOTE_CONTROL_MENU_STATES,
|
||||||
RemoteControlButton,
|
RemoteVideoMenuTriggerButton
|
||||||
RemoteVideoMenu,
|
|
||||||
VolumeSlider
|
|
||||||
} from '../../../react/features/remote-video-menu';
|
} from '../../../react/features/remote-video-menu';
|
||||||
/* eslint-enable no-unused-vars */
|
/* eslint-enable no-unused-vars */
|
||||||
|
|
||||||
|
@ -21,13 +20,7 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||||
|
|
||||||
import SmallVideo from "./SmallVideo";
|
import SmallVideo from "./SmallVideo";
|
||||||
import UIUtils from "../util/UIUtil";
|
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
|
const ParticipantConnectionStatus
|
||||||
= JitsiMeetJS.constants.participantConnectionStatus;
|
= JitsiMeetJS.constants.participantConnectionStatus;
|
||||||
|
|
||||||
|
@ -49,6 +42,8 @@ function RemoteVideo(user, VideoLayout, emitter) {
|
||||||
this._audioStreamElement = null;
|
this._audioStreamElement = null;
|
||||||
this.hasRemoteVideoMenu = false;
|
this.hasRemoteVideoMenu = false;
|
||||||
this._supportsRemoteControl = false;
|
this._supportsRemoteControl = false;
|
||||||
|
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
|
||||||
|
? 'left top' : 'top center';
|
||||||
this.addRemoteVideoContainer();
|
this.addRemoteVideoContainer();
|
||||||
this.updateIndicators();
|
this.updateIndicators();
|
||||||
this.setDisplayName();
|
this.setDisplayName();
|
||||||
|
@ -78,8 +73,6 @@ function RemoteVideo(user, VideoLayout, emitter) {
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
// 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
|
// TODO The event handlers should be turned into actions so changes can be
|
||||||
// handled through reducers and middleware.
|
// handled through reducers and middleware.
|
||||||
this._kickHandler = this._kickHandler.bind(this);
|
|
||||||
this._muteHandler = this._muteHandler.bind(this);
|
|
||||||
this._requestRemoteControlPermissions
|
this._requestRemoteControlPermissions
|
||||||
= this._requestRemoteControlPermissions.bind(this);
|
= this._requestRemoteControlPermissions.bind(this);
|
||||||
this._setAudioVolume = this._setAudioVolume.bind(this);
|
this._setAudioVolume = this._setAudioVolume.bind(this);
|
||||||
|
@ -107,39 +100,6 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
|
||||||
return this.container;
|
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
|
* 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
|
* if the mouse is over the video, or if the connection indicator or the popup
|
||||||
|
@ -160,6 +120,17 @@ RemoteVideo.prototype._isHovered = function () {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
RemoteVideo.prototype._generatePopupContent = function () {
|
RemoteVideo.prototype._generatePopupContent = function () {
|
||||||
|
if (interfaceConfig.filmStripOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteVideoMenuContainer
|
||||||
|
= this.container.querySelector('.remotevideomenu');
|
||||||
|
|
||||||
|
if (!remoteVideoMenuContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { controller } = APP.remoteControl;
|
const { controller } = APP.remoteControl;
|
||||||
let remoteControlState = null;
|
let remoteControlState = null;
|
||||||
let onRemoteControlToggle;
|
let onRemoteControlToggle;
|
||||||
|
@ -189,35 +160,28 @@ RemoteVideo.prototype._generatePopupContent = function () {
|
||||||
const participantID = this.id;
|
const participantID = this.id;
|
||||||
|
|
||||||
/* jshint ignore:start */
|
/* jshint ignore:start */
|
||||||
return (
|
ReactDOM.render(
|
||||||
<RemoteVideoMenu id = { participantID }>
|
<Provider store = { APP.store }>
|
||||||
{ isModerator
|
<I18nextProvider i18n = { i18next }>
|
||||||
? <MuteButton
|
<RemoteVideoMenuTriggerButton
|
||||||
|
initialVolumeValue = { initialVolumeValue }
|
||||||
isAudioMuted = { this.isAudioMuted }
|
isAudioMuted = { this.isAudioMuted }
|
||||||
onClick = { this._muteHandler }
|
isModerator = { isModerator }
|
||||||
participantID = { participantID } />
|
onMenuDisplay = { this._onRemoteVideoMenuDisplay.bind(this) }
|
||||||
: null }
|
onRemoteControlToggle = { onRemoteControlToggle }
|
||||||
{ isModerator
|
onVolumeChange = { onVolumeChange }
|
||||||
? <KickButton
|
|
||||||
onClick = { this._kickHandler }
|
|
||||||
participantID = { participantID } />
|
|
||||||
: null }
|
|
||||||
{ remoteControlState
|
|
||||||
? <RemoteControlButton
|
|
||||||
onClick = { onRemoteControlToggle }
|
|
||||||
participantID = { participantID }
|
participantID = { participantID }
|
||||||
remoteControlState = { remoteControlState } />
|
remoteControlState = { remoteControlState } />
|
||||||
: null }
|
</I18nextProvider>
|
||||||
{ onVolumeChange
|
</Provider>,
|
||||||
? <VolumeSlider
|
remoteVideoMenuContainer);
|
||||||
initialValue = { initialVolumeValue }
|
|
||||||
onChange = { onVolumeChange } />
|
|
||||||
: null }
|
|
||||||
</RemoteVideoMenu>
|
|
||||||
);
|
|
||||||
/* jshint ignore:end */
|
/* jshint ignore:end */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RemoteVideo.prototype._onRemoteVideoMenuDisplay = function () {
|
||||||
|
this.updateRemoteVideoMenu(this.isAudioMuted, true);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the remote control supported value and initializes or updates the menu
|
* Sets the remote control supported value and initializes or updates the menu
|
||||||
* depending on the remote control is supported or not.
|
* depending on the remote control is supported or not.
|
||||||
|
@ -288,27 +252,6 @@ RemoteVideo.prototype._stopRemoteControl = function () {
|
||||||
this.updateRemoteVideoMenu(this.isAudioMuted, true);
|
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.
|
* 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 isMuted the new muted state to update to
|
||||||
* @param force to work even if popover is not visible
|
* @param force to work even if popover is not visible
|
||||||
*/
|
*/
|
||||||
RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
|
RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) {
|
||||||
this.isAudioMuted = isMuted;
|
this.isAudioMuted = isMuted;
|
||||||
|
|
||||||
if (!this.popover) {
|
this._generatePopupContent();
|
||||||
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());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -395,17 +330,9 @@ RemoteVideo.prototype.addRemoteVideoMenu = function () {
|
||||||
if (interfaceConfig.filmStripOnly) {
|
if (interfaceConfig.filmStripOnly) {
|
||||||
return;
|
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;
|
this.hasRemoteVideoMenu = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -538,6 +465,8 @@ RemoteVideo.prototype.remove = function () {
|
||||||
|
|
||||||
this._unmountIndicators();
|
this._unmountIndicators();
|
||||||
|
|
||||||
|
this.removeRemoteVideoMenu();
|
||||||
|
|
||||||
// Make sure that the large video is updated if are removing its
|
// Make sure that the large video is updated if are removing its
|
||||||
// corresponding small video.
|
// corresponding small video.
|
||||||
this.VideoLayout.updateAfterThumbRemoved(this.id);
|
this.VideoLayout.updateAfterThumbRemoved(this.id);
|
||||||
|
@ -591,17 +520,29 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
||||||
|
|
||||||
// Add click handler.
|
// Add click handler.
|
||||||
let onClickHandler = (event) => {
|
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
|
const clickedOnPopover
|
||||||
if ($(source).parents('.popupmenu').length === 0) {
|
= $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);
|
this.VideoLayout.handleVideoThumbClicked(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On IE we need to populate this handler on video <object>
|
// On IE we need to populate this handler on video <object>
|
||||||
// and it does not give event instance as an argument,
|
// and it does not give event instance as an argument,
|
||||||
// so we check here for methods.
|
// so we check here for methods.
|
||||||
if (event.stopPropagation && event.preventDefault) {
|
if (event.stopPropagation && event.preventDefault && !ignoreClick) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -665,8 +606,9 @@ RemoteVideo.prototype.setDisplayName = function(displayName) {
|
||||||
*/
|
*/
|
||||||
RemoteVideo.prototype.removeRemoteVideoMenu = function() {
|
RemoteVideo.prototype.removeRemoteVideoMenu = function() {
|
||||||
var menuSpan = $('#' + this.videoSpanId + '> .remotevideomenu');
|
var menuSpan = $('#' + this.videoSpanId + '> .remotevideomenu');
|
||||||
|
|
||||||
if (menuSpan.length) {
|
if (menuSpan.length) {
|
||||||
this.popover.forceHide();
|
ReactDOM.unmountComponentAtNode(menuSpan.get(0));
|
||||||
menuSpan.remove();
|
menuSpan.remove();
|
||||||
this.hasRemoteVideoMenu = false;
|
this.hasRemoteVideoMenu = false;
|
||||||
}
|
}
|
||||||
|
@ -740,30 +682,12 @@ RemoteVideo.createContainer = function (spanId) {
|
||||||
presenceLabelContainer.className = 'presence-label-container';
|
presenceLabelContainer.className = 'presence-label-container';
|
||||||
container.appendChild(presenceLabelContainer);
|
container.appendChild(presenceLabelContainer);
|
||||||
|
|
||||||
|
const remoteVideoMenuContainer = document.createElement('span');
|
||||||
|
remoteVideoMenuContainer.className = 'remotevideomenu';
|
||||||
|
container.appendChild(remoteVideoMenuContainer);
|
||||||
|
|
||||||
var remotes = document.getElementById('filmstripRemoteVideosContainer');
|
var remotes = document.getElementById('filmstripRemoteVideosContainer');
|
||||||
return remotes.appendChild(container);
|
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;
|
export default RemoteVideo;
|
||||||
|
|
|
@ -747,21 +747,24 @@ SmallVideo.prototype.updateIndicators = function () {
|
||||||
|
|
||||||
/* jshint ignore:start */
|
/* jshint ignore:start */
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<div>
|
<I18nextProvider i18n = { i18next }>
|
||||||
{ this._showConnectionIndicator
|
<div>
|
||||||
? <ConnectionIndicator
|
{ this._showConnectionIndicator
|
||||||
connectionStatus = { this._connectionStatus }
|
? <ConnectionIndicator
|
||||||
iconSize = { iconSize }
|
connectionStatus = { this._connectionStatus }
|
||||||
isLocalVideo = { this.isLocal }
|
isLocalVideo = { this.isLocal }
|
||||||
onHover = { this._onPopoverHover }
|
enableStatsDisplay = { !interfaceConfig.filmStripOnly }
|
||||||
showMoreLink = { this.isLocal }
|
statsPopoverPosition = { this.statsPopoverLocation }
|
||||||
userID = { this.id } />
|
userID = { this.id } />
|
||||||
: null }
|
: null }
|
||||||
{ this._showRaisedHand
|
{ this._showRaisedHand
|
||||||
? <RaisedHandIndicator iconSize = { iconSize } /> : null }
|
? <RaisedHandIndicator iconSize = { iconSize } />
|
||||||
{ this._showDominantSpeaker
|
: null }
|
||||||
? <DominantSpeakerIndicator iconSize = { iconSize } /> : null }
|
{ this._showDominantSpeaker
|
||||||
</div>,
|
? <DominantSpeakerIndicator iconSize = { iconSize } />
|
||||||
|
: null }
|
||||||
|
</div>
|
||||||
|
</I18nextProvider>,
|
||||||
indicatorToolbar
|
indicatorToolbar
|
||||||
);
|
);
|
||||||
/* jshint ignore:end */
|
/* jshint ignore:end */
|
||||||
|
|
|
@ -10,6 +10,26 @@
|
||||||
*/
|
*/
|
||||||
export const DOMINANT_SPEAKER_CHANGED = Symbol('DOMINANT_SPEAKER_CHANGED');
|
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.
|
* Create an action for when the local participant's display name is updated.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import {
|
import {
|
||||||
DOMINANT_SPEAKER_CHANGED,
|
DOMINANT_SPEAKER_CHANGED,
|
||||||
|
KICK_PARTICIPANT,
|
||||||
|
MUTE_REMOTE_PARTICIPANT,
|
||||||
PARTICIPANT_DISPLAY_NAME_CHANGED,
|
PARTICIPANT_DISPLAY_NAME_CHANGED,
|
||||||
PARTICIPANT_ID_CHANGED,
|
PARTICIPANT_ID_CHANGED,
|
||||||
PARTICIPANT_JOINED,
|
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
|
* 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
|
* 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.
|
* Action to update a participant's connection status.
|
||||||
*
|
*
|
||||||
|
|
|
@ -7,7 +7,11 @@ import {
|
||||||
import { MiddlewareRegistry } from '../redux';
|
import { MiddlewareRegistry } from '../redux';
|
||||||
|
|
||||||
import { localParticipantIdChanged } from './actions';
|
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 { LOCAL_PARTICIPANT_DEFAULT_ID } from './constants';
|
||||||
import { getLocalParticipant } from './functions';
|
import { getLocalParticipant } from './functions';
|
||||||
|
|
||||||
|
@ -30,6 +34,32 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
store.dispatch(localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
|
store.dispatch(localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
|
||||||
break;
|
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
|
// TODO Remove this middleware when the local display name update flow is
|
||||||
// fully brought into redux.
|
// fully brought into redux.
|
||||||
case PARTICIPANT_DISPLAY_NAME_CHANGED: {
|
case PARTICIPANT_DISPLAY_NAME_CHANGED: {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
|
import AKInlineDialog from '@atlaskit/inline-dialog';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import JitsiPopover from '../../../../modules/UI/util/JitsiPopover';
|
|
||||||
|
|
||||||
import { JitsiParticipantConnectionStatus } from '../../base/lib-jitsi-meet';
|
import { JitsiParticipantConnectionStatus } from '../../base/lib-jitsi-meet';
|
||||||
import { ConnectionStatsTable } from '../../connection-stats';
|
import { ConnectionStatsTable } from '../../connection-stats';
|
||||||
|
|
||||||
|
@ -67,21 +66,22 @@ class ConnectionIndicator extends Component {
|
||||||
*/
|
*/
|
||||||
connectionStatus: React.PropTypes.string,
|
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.
|
* Whether or not the displays stats are for local video.
|
||||||
*/
|
*/
|
||||||
isLocalVideo: React.PropTypes.bool,
|
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,
|
statsPopoverPosition: React.PropTypes.string,
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the popover should display a link that can toggle
|
|
||||||
* a more detailed view of the stats.
|
|
||||||
*/
|
|
||||||
showMoreLink: React.PropTypes.bool,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked to obtain translated strings.
|
* Invoked to obtain translated strings.
|
||||||
|
@ -104,16 +104,6 @@ class ConnectionIndicator extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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 = {
|
this.state = {
|
||||||
/**
|
/**
|
||||||
* Whether or not the popover content should display additional
|
* 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.
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
this._onStatsUpdated = this._onStatsUpdated.bind(this);
|
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._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
|
* @inheritdoc
|
||||||
* returns {void}
|
* returns {void}
|
||||||
|
@ -147,20 +139,10 @@ class ConnectionIndicator extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
statsEmitter.subscribeToClientStats(
|
statsEmitter.subscribeToClientStats(
|
||||||
this.props.userID, this._onStatsUpdated);
|
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
|
* Updates which user's stats are being listened to.
|
||||||
* popover is not a React Component yet and so is not automatiucally aware
|
|
||||||
* of changed data.
|
|
||||||
*
|
*
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
* returns {void}
|
* returns {void}
|
||||||
|
@ -172,22 +154,17 @@ class ConnectionIndicator extends Component {
|
||||||
statsEmitter.subscribeToClientStats(
|
statsEmitter.subscribeToClientStats(
|
||||||
this.props.userID, this._onStatsUpdated);
|
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
|
* @private
|
||||||
* returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
statsEmitter.unsubscribeToClientStats(
|
statsEmitter.unsubscribeToClientStats(
|
||||||
this.props.userID, this._onStatsUpdated);
|
this.props.userID, this._onStatsUpdated);
|
||||||
|
|
||||||
this.popover.forceHide();
|
|
||||||
this.popover.remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -198,16 +175,48 @@ class ConnectionIndicator extends Component {
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className = 'connection-indicator-container'>
|
||||||
className = 'connection-indicator indicator'
|
<AKInlineDialog
|
||||||
ref = { this._setRootElement }>
|
content = { this._renderStatisticsTable() }
|
||||||
<div className = 'connection indicatoricon'>
|
isOpen = { this.state.showStats }
|
||||||
{ this._renderIcon() }
|
onClose = { this._onStatsClose }
|
||||||
</div>
|
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>
|
</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
|
* Callback invoked when new connection stats associated with the passed in
|
||||||
* user ID are available. Will update the component's display of current
|
* user ID are available. Will update the component's display of current
|
||||||
|
@ -314,17 +323,6 @@ class ConnectionIndicator extends Component {
|
||||||
transport = { transport } />
|
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;
|
export default ConnectionIndicator;
|
||||||
|
|
|
@ -292,7 +292,7 @@ class ConnectionStatsTable extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className = 'jitsipopover__showmore link'
|
className = 'showmore link'
|
||||||
onClick = { this.props.onShowMore } >
|
onClick = { this.props.onShowMore } >
|
||||||
{ this.props.t(translationKey) }
|
{ this.props.t(translationKey) }
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -27,7 +27,7 @@ class BaseIndicator extends Component {
|
||||||
iconClassName: React.PropTypes.string,
|
iconClassName: React.PropTypes.string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The front size for the icon.
|
* The font size for the icon.
|
||||||
*/
|
*/
|
||||||
iconSize: React.PropTypes.string,
|
iconSize: React.PropTypes.string,
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
|
import { kickParticipant } from '../../base/participants';
|
||||||
|
|
||||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||||
|
|
||||||
|
@ -18,7 +20,13 @@ class KickButton extends Component {
|
||||||
*/
|
*/
|
||||||
static propTypes = {
|
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,
|
onClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
@ -33,6 +41,19 @@ class KickButton extends Component {
|
||||||
t: React.PropTypes.func
|
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()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -40,16 +61,32 @@ class KickButton extends Component {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { onClick, participantID, t } = this.props;
|
const { participantID, t } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteVideoMenuButton
|
<RemoteVideoMenuButton
|
||||||
buttonText = { t('videothumbnail.kick') }
|
buttonText = { t('videothumbnail.kick') }
|
||||||
iconClass = 'icon-kick'
|
iconClass = 'icon-kick'
|
||||||
id = { `ejectlink_${participantID}` }
|
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 React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
|
import { muteRemoteParticipant } from '../../base/participants';
|
||||||
|
|
||||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||||
|
|
||||||
|
@ -17,13 +19,19 @@ class MuteButton extends Component {
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
static propTypes = {
|
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.
|
* Whether or not the participant is currently audio muted.
|
||||||
*/
|
*/
|
||||||
isAudioMuted: React.PropTypes.bool,
|
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,
|
onClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
@ -38,6 +46,19 @@ class MuteButton extends Component {
|
||||||
t: React.PropTypes.func
|
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()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -45,7 +66,7 @@ class MuteButton extends Component {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { isAudioMuted, onClick, participantID, t } = this.props;
|
const { isAudioMuted, participantID, t } = this.props;
|
||||||
const muteConfig = isAudioMuted ? {
|
const muteConfig = isAudioMuted ? {
|
||||||
translationKey: 'videothumbnail.muted',
|
translationKey: 'videothumbnail.muted',
|
||||||
muteClassName: 'mutelink disabled'
|
muteClassName: 'mutelink disabled'
|
||||||
|
@ -60,9 +81,26 @@ class MuteButton extends Component {
|
||||||
displayClass = { muteConfig.muteClassName }
|
displayClass = { muteConfig.muteClassName }
|
||||||
iconClass = 'icon-mic-disabled'
|
iconClass = 'icon-mic-disabled'
|
||||||
id = { `mutelink_${participantID}` }
|
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
|
default as RemoteControlButton
|
||||||
} from './RemoteControlButton';
|
} from './RemoteControlButton';
|
||||||
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
|
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
|
||||||
|
export {
|
||||||
|
default as RemoteVideoMenuTriggerButton
|
||||||
|
} from './RemoteVideoMenuTriggerButton';
|
||||||
export { default as VolumeSlider } from './VolumeSlider';
|
export { default as VolumeSlider } from './VolumeSlider';
|
||||||
|
|
Loading…
Reference in New Issue