feat(thumbnail) Video thumbnails redesign and refactor (#10351)

Update video thumbnail design
Update design of indicators
In filmstrip view move Screen Sharing indicator to the top
Removed dominant speaker indicator
Use ContextMenu component for the connection stats popover
Combine Remove video menu and Meeting participant context menu into one component
Moved some styles from SCSS to JSS
Fix mobile avatars too big
Fix mobile horizontal scroll
Created button for Send to breakout room action
This commit is contained in:
Robert Pintilii 2021-12-15 15:18:41 +02:00 committed by GitHub
parent 0503a83667
commit 91437c50e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1875 additions and 2571 deletions

View File

@ -24,14 +24,6 @@
bottom: calc(#{$newToolbarSizeWithPadding}) !important;
}
/**
* Override @atlaskit/theme styling for the top toolbar so it displays over
* the video thumbnail while obscuring as little as possible.
*/
.videocontainer__toptoolbar > div > div {
background: none;
}
/**
* Keep overflow menu within screen vertical bounds and make it scrollable.

View File

@ -42,15 +42,6 @@
}
}
.popupmenu {
margin: auto;
width: 100%;
}
.popupmenu__item {
height: 48px;
}
&#{&} .overflow-menu {
margin: auto;
font-size: 1.2em;

View File

@ -43,10 +43,5 @@
.popover {
margin: -16px -24px;
padding: 16px 24px;
z-index: $popoverZ;
}
.padded-content {
padding: 4px 8px;
}

View File

@ -2,78 +2,8 @@
* Initialize
**/
.popupmenu {
background-color: $menuBG;
border-radius: 3px;
list-style-type: none;
min-width: 150px;
text-align: left;
padding: 0px;
white-space: nowrap;
&__item {
list-style-type: none;
height: 35px;
}
// Link Appearance
&__link,
&__contents {
display: block;
box-sizing: border-box;
text-decoration: none;
height: 100%;
font-size: 9pt;
width: 100%;
cursor: pointer;
padding: 0 5px;
color: $popupMenuColor;
&:hover {
background-color: $popupMenuHoverBackground;
color: $popupMenuHoverColor;
}
&.disabled {
pointer-events: none;
}
}
&__list {
margin: 0;
padding: 0;
}
&__text {
display: inline-block;
margin-left: 8px;
vertical-align: middle;
}
&__link {
i {
cursor: pointer;
}
}
&__contents {
display: flex;
/**
* Positioning styles on the slider and its container are used to make
* the container fit the popup width, by removing the slider from the
* page flow, and then making the slider fit the container.
*/
.popupmenu__slider_container {
position: relative;
width: 100%;
.popupmenu__slider {
position: absolute;
top: 50%;
transform: translate(0, -50%);
width: 100%;
.popupmenu__contents {
.popupmenu__volume-slider {
&::-webkit-slider-runnable-track {
background-color: $popupSliderColor;
}
@ -87,37 +17,3 @@
}
}
}
}
&__icon {
vertical-align: middle;
position: relative;
display: inline-block;
min-width: 20px;
height: 100%;
padding-right: 10px;
> * {
@include absoluteAligning();
}
}
.icon-kick,
.icon-play,
.icon-stop {
font-size: 8pt;
}
}
/**
* Override reset css styling modifying all lists and set negative margin to
* reduce the visibility of padding on AtlasKit
* InlineDialogs.
*/
ul.popupmenu {
margin: -16px -24px;
}
span.localvideomenu:hover ul.popupmenu, span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
display:block !important;
}

View File

@ -13,9 +13,6 @@ $hangupFontSize: 2em;
*/
// Video layout.
$thumbnailToolbarHeight: 22px;
$thumbnailIndicatorBorder: 2px;
$thumbnailIndicatorSize: $thumbnailToolbarHeight;
$thumbnailVideoMargin: 2px;
$thumbnailsBorder: 2px;
$thumbnailVideoBorder: 2px;
@ -56,19 +53,12 @@ $overflowMenuItemBackground: #36383C;
/**
* Video layout
*/
$videoThumbnailHovered: rgba(22, 94, 204, .4);
$videoThumbnailSelected: #165ECC;
$participantNameColor: #fff;
$thumbnailPictogramColor: #fff;
$dominantSpeakerBg: #165ecc;
$raiseHandBg: #F8AE1A;
$audioLevelBg: #44A5FF;
$connectionIndicatorBg: #165ecc;
$audioLevelShadow: rgba(9, 36, 77, 0.9);
$videoStateIndicatorColor: $defaultColor;
$videoStateIndicatorBackground: $toolbarBackground;
$videoStateIndicatorSize: 40px;
$remoteVideoMenuIconMargin: initial;
/**
* Feedback Modal
@ -102,7 +92,6 @@ $sidebarWidth: 315px;
* Misc.
*/
$borderRadius: 4px;
$popoverMenuPadding: 13px;
$happySoftwareBackground: transparent;
$desktopAppDragBarHeight: 25px;
$scrollHeight: 7px;
@ -118,7 +107,6 @@ $toolbarBackgroundZ: 4;
$labelsZ: 5;
$subtitlesZ: 7;
$popoverZ: 8;
$zindex10: 10;
$reloadZ: 20;
$poweredByZ: 100;
$ringingZ: 300;

View File

@ -43,165 +43,7 @@
.videocontainer {
position: relative;
text-align: center;
&__background {
@include topLeft();
background-color: black;
border-radius: $borderRadius;
width: 100%;
height: 100%;
}
/**
* The toolbar of the video thumbnail.
*/
&__toolbar,
&__toptoolbar {
position: absolute;
left: 0;
pointer-events: none;
z-index: $zindex10;
width: 100%;
box-sizing: border-box; // Includes the padding in the 100% width.
/**
* FIXME (lenny): Disabling pointer-events is a pretty big sin that
* sidesteps the problems. There are z-index wars occurring within
* videocontainer and AtlasKit Tooltips rely on their parent z-indexe
* being higher than whatever they need to appear over. So set a higher
* z-index for the tooltip containers but make any empty space not block
* mouse overs for various mouseover triggers.
*/
pointer-events: none;
* {
pointer-events: auto;
}
.indicator-container {
display: inline-block;
float: left;
pointer-events: all;
}
}
&__toolbar {
bottom: 0;
padding: 0 5px 0 5px;
}
&__toptoolbar {
$toolbarIconMargin: 5px;
top: 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-left: 5px;
margin-top: $toolbarIconMargin;
}
.indicator-container:nth-child(1) .indicator {
margin-left: $toolbarIconMargin;
}
.indicator-container {
display: inline-block;
vertical-align: top;
.popover-trigger {
display: inline-block;
}
}
.connection-indicator,
.indicator {
position: relative;
font-size: 8px;
text-align: center;
line-height: $thumbnailIndicatorSize;
padding: 0;
@include circle($thumbnailIndicatorSize);
box-sizing: border-box;
z-index: $zindex3;
background: $dominantSpeakerBg;
color: $thumbnailPictogramColor;
border: $thumbnailIndicatorBorder solid $thumbnailPictogramColor;
.indicatoricon {
@include absoluteAligning();
}
.connection {
position: relative;
display: inline-block;
margin: 0 auto;
left: 0;
@include transform(translate(0, -50%));
&_empty,
&_lost
{
color: #8B8B8B;/*#FFFFFF*/
overflow: hidden;
}
&_full
{
@include topLeft();
color: #FFFFFF;/*#15A1ED*/
overflow: hidden;
}
&_ninja
{
font-size: 1.5em;
}
}
.icon-gsm-bars {
cursor: pointer;
font-size: 1em;
}
}
.hide-connection-indicator {
display: none;
}
}
&__hoverOverlay {
background: rgba(0,0,0,.6);
border-radius: $borderRadius;
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
visibility: hidden;
z-index: $zindex2;
}
&__participant-name {
color: #fff;
background-color: rgba(0,0,0,.4);
padding: 3px 7px;
border-radius: 3px;
max-width: calc(100% - 32px);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
height: 16px;
display: inline-block;
text-align: right;
}
overflow: 'hidden';
@media (min-width: 581px) {
&.shift-right {
@ -288,16 +130,6 @@
z-index: $zindex0;
}
/**
* Positions video thumbnail display name and editor.
*/
#alwaysOnTop .displayname,
.videocontainer .displayname,
.videocontainer .editdisplayname {
font-weight: 100;
color: $participantNameColor;
}
#alwaysOnTop .displayname {
font-size: 15px;
position: inherit;
@ -307,146 +139,6 @@
margin-top: 10px;
}
/**
* Positions video thumbnail display name editor.
*/
.videocontainer .editdisplayname {
outline: none;
border: none;
background: none;
box-shadow: none;
padding: 0;
}
#localVideoContainer .displayname:hover {
cursor: text;
}
.videocontainer .displayname {
pointer-events: none;
padding: 0 3px 0 3px;
}
.videocontainer .editdisplayname {
height: auto;
}
#localDisplayName {
pointer-events: auto !important;
}
.videocontainer>a.displayname {
display: inline-block;
position: absolute;
color: #FFFFFF;
bottom: 0;
right: 0;
padding: 3px 5px;
font-size: 9pt;
cursor: pointer;
z-index: $zindex2;
}
/**
* Video thumbnail toolbar icon.
*/
.videocontainer .toolbar-icon {
font-size: 8pt;
text-align: center;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
color: #FFFFFF;
width: 12px;
line-height: $thumbnailToolbarHeight;
height: $thumbnailToolbarHeight;
padding: 0;
border: 0;
margin: 0px 5px 0px 0px;
}
/**
* Toolbar icon internal i elements (font icons).
*/
.toolbar-icon>div {
height: $thumbnailToolbarHeight;
display: flex;
flex-direction: column;
justify-content: center;
}
/**
* Toolbar icons positioned on the right.
*/
.moderator-icon {
display: inline-block;
&.right {
float: right;
margin: 0px 0px 0px 5px;
}
.toolbar-icon {
margin: 0;
}
}
.raisehandindicator {
background: $raiseHandBg !important;
}
.connection-indicator {
background: $connectionIndicatorBg;
&.status-high {
background: green;
}
&.status-med {
background: #FFD740;
}
&.status-lost {
background: gray;
}
&.status-low {
background: #BF2117;
}
&.status-other {
background: $connectionIndicatorBg;
}
&.status-disabled {
background: transparent;
border: none
}
}
.local-video-menu-trigger,
.remote-video-menu-trigger,
.localvideomenu,
.remotevideomenu
{
display: inline-block;
position: absolute;
top: 0px;
right: 0;
z-index: $zindex2;
width: 18px;
height: 18px;
color: #FFF;
font-size: 10pt;
margin-right: $remoteVideoMenuIconMargin;
>i{
cursor: hand;
}
}
.local-video-menu-trigger,
.remote-video-menu-trigger {
margin-top: 7px;
}
/**
* Audio indicator on video thumbnails.
*/
@ -623,74 +315,11 @@
display: none;
}
.display-avatar-with-name {
.avatar-container {
visibility: visible;
}
.displayNameContainer {
visibility: visible;
}
.videocontainer__hoverOverlay {
visibility: visible;
}
video {
visibility: hidden;
}
}
.display-name-on-black {
.avatar-container {
visibility: hidden;
}
.displayNameContainer {
visibility: visible;
}
.videocontainer__hoverOverlay {
visibility: hidden;
}
video {
opacity: 0.2;
visibility: visible;
}
}
.display-video {
.avatar-container {
visibility: hidden;
}
.displayNameContainer {
visibility: hidden;
}
.videocontainer__hoverOverlay {
visibility: hidden;
}
video {
visibility: visible;
}
}
.display-name-on-video {
.avatar-container {
visibility: hidden;
}
.displayNameContainer {
visibility: visible;
}
.videocontainer__hoverOverlay {
visibility: visible;
}
video {
visibility: visible;
}
@ -701,14 +330,6 @@
visibility: visible;
}
.displayNameContainer {
visibility: hidden;
}
.videocontainer__hoverOverlay {
visibility: hidden;
}
video {
visibility: hidden;
}

View File

@ -6,37 +6,10 @@
border-radius: $borderRadius;
margin: 0 $thumbnailVideoMargin;
&.videoContainerFocused, &:hover {
&:hover {
cursor: hand;
}
/**
* Focused video thumbnail.
*/
&.videoContainerFocused {
border: $thumbnailVideoBorder solid $videoThumbnailSelected;
box-shadow: inset 0 0 3px $videoThumbnailSelected,
0 0 3px $videoThumbnailSelected;
}
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
display: none;
}
/**
* Hovered video thumbnail.
*/
&:hover:not(.videoContainerFocused):not(.active-speaker) {
cursor: hand;
border: $thumbnailVideoBorder solid $videoThumbnailHovered;
box-shadow: inset 0 0 3px $videoThumbnailHovered,
0 0 3px $videoThumbnailHovered;
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
display: inline-block;
}
}
& > video {
cursor: hand;
border-radius: $borderRadius;

View File

@ -2,13 +2,6 @@
* CSS styles that are specific to the filmstrip that shows the thumbnail tiles.
*/
.tile-view {
/**
* Add a border around the active speaker to make the thumbnail easier to
* see.
*/
.active-speaker {
box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px $videoThumbnailSelected;
}
.remote-videos {
align-items: center;
@ -134,7 +127,3 @@
}
}
}
.indicator-icon-container {
display: inline-block;
}

View File

@ -35,16 +35,4 @@
#remotePresenceMessage {
display: none !important;
}
/**
* Thumbnail popover menus can overlap other thumbnails. Setting an auto
* z-index will allow AtlasKit InlineDialog's large z-index to be
* respected and thereby display over elements in other thumbnails,
* specifically the various status icons.
*/
.remotevideomenu,
.localvideomenu,
.videocontainer__toptoolbar {
z-index: auto;
}
}

View File

@ -19,72 +19,6 @@
* Overrides for small videos in vertical filmstrip mode.
*/
.vertical-filmstrip .filmstrip__videos .videocontainer {
/**
* Move status icons to the bottom right of the thumbnail.
*/
.videocontainer__toolbar {
/**
* FIXME: disable pointer to allow any elements moved below to still
* be clickable. The real fix would to make sure those moved elements
* are actually part of the toolbar instead of positioning being faked.
*/
pointer-events: none;
text-align: right;
> div {
pointer-events: none;
}
.right {
float: none;
margin: auto;
}
.toolbar-icon {
pointer-events: all;
}
}
/**
* 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,
.local-video-menu-trigger,
.remote-video-menu-trigger,
.indicator-icon-container {
transform: translate3d(0, 0, 0);
}
.indicator-icon-container {
display: inline-block;
}
.indicator-container {
float: none;
}
/**
* Move the remote video menu trigger to the bottom left of the video
* thumbnail.
*/
.localvideomenu,
.remotevideomenu,
.local-video-menu-trigger,
.remote-video-menu-trigger {
bottom: 0;
left: 0;
top: auto;
right: auto;
}
.local-video-menu-trigger,
.remote-video-menu-trigger {
margin-bottom: 3px;
margin-left: $remoteVideoMenuIconMargin;
}
.self-view-mobile-portrait video {
object-fit: contain;
}

View File

@ -75,11 +75,7 @@ $errorColor: #c61600;
$feedbackCancelFontColor: #333;
// Popover colors
$popoverBg: initial;
$popoverFontColor: #ffffff !important;
$popupMenuColor: #ffffff !important;
$popupMenuHoverColor: #ffffff !important;
$popupMenuHoverBackground: rgba(255, 255, 255, 0.1);
$popupSliderColor: #0376da;
// Toolbar

View File

@ -26,7 +26,7 @@ var interfaceConfig = {
CLOSE_PAGE_GUEST_HINT: false, // A html text to be shown to guests on the close page, false disables it
DEFAULT_BACKGROUND: '#474747',
DEFAULT_BACKGROUND: '#040404',
DEFAULT_LOGO_URL: 'images/watermark.svg',
DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.svg',

View File

@ -949,8 +949,8 @@
"mute": "Mute / Unmute",
"muteEveryone": "Mute everyone",
"muteEveryoneElse": "Mute everyone else",
"muteEveryonesVideo": "Disable everyone's video",
"muteEveryoneElsesVideo": "Disable everyone else's video",
"muteEveryonesVideoStream": "Stop everyone's video",
"muteEveryoneElsesVideoStream": "Stop everyone else's video",
"participants": "Participants",
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",

View File

@ -19,7 +19,7 @@ type Props = {
/**
* Class name for context menu. Used to overwrite default styles.
*/
className?: string,
className?: ?string,
/**
* The entity for which the context menu is displayed.
@ -31,10 +31,15 @@ type Props = {
*/
hidden?: boolean,
/**
* Whether or not the menu is already in a drawer.
*/
inDrawer?: ?boolean,
/**
* Whether or not drawer should be open.
*/
isDrawerOpen: boolean,
isDrawerOpen?: boolean,
/**
* Target elements against which positioning calculations are made.
@ -49,7 +54,7 @@ type Props = {
/**
* Callback for drawer close.
*/
onDrawerClose: Function,
onDrawerClose?: Function,
/**
* Callback for the mouse entering the component.
@ -59,7 +64,7 @@ type Props = {
/**
* Callback for the mouse leaving the component.
*/
onMouseLeave: Function
onMouseLeave?: Function
};
const useStyles = makeStyles(theme => {
@ -106,6 +111,7 @@ const ContextMenu = ({
className,
entity,
hidden,
inDrawer,
isDrawerOpen,
offsetTarget,
onClick,
@ -147,6 +153,14 @@ const ContextMenu = ({
}
}, [ hidden ]);
if (_overflowDrawer && inDrawer) {
return (<div
className = { styles.drawer }
onClick = { onDrawerClose }>
{children}
</div>);
}
return _overflowDrawer
? <JitsiPortal>
<Drawer

View File

@ -0,0 +1,129 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React from 'react';
import { useSelector } from 'react-redux';
import { showOverflowDrawer } from '../../../toolbox/functions.web';
import { Icon } from '../../icons';
export type Props = {
/**
* Label used for accessibility.
*/
accessibilityLabel: string,
/**
* CSS class name used for custom styles.
*/
className?: string,
/**
* Custom icon. If used, the icon prop is ignored.
* Used to allow custom children instead of just the default icons.
*/
customIcon?: React$Node,
/**
* Whether or not the action is disabled.
*/
disabled?: boolean,
/**
* Id of the action container.
*/
id?: string,
/**
* Default icon for action.
*/
icon?: Function,
/**
* Click handler.
*/
onClick?: Function,
/**
* Action text.
*/
text: string,
/**
* Class name for the text.
*/
textClassName?: string
}
const useStyles = makeStyles(theme => {
return {
contextMenuItem: {
alignItems: 'center',
cursor: 'pointer',
display: 'flex',
minHeight: '40px',
padding: '10px 16px',
boxSizing: 'border-box',
'& > *:not(:last-child)': {
marginRight: `${theme.spacing(3)}px`
},
'&:hover': {
backgroundColor: theme.palette.ui04
}
},
contextMenuItemDisabled: {
pointerEvents: 'none'
},
contextMenuItemDrawer: {
padding: '12px 16px'
},
contextMenuItemIcon: {
'& svg': {
fill: theme.palette.icon01
}
}
};
});
const ContextMenuItem = ({
accessibilityLabel,
className,
customIcon,
disabled,
id,
icon,
onClick,
text,
textClassName }: Props) => {
const styles = useStyles();
const _overflowDrawer = useSelector(showOverflowDrawer);
return (
<div
aria-label = { accessibilityLabel }
className = { clsx(styles.contextMenuItem,
_overflowDrawer && styles.contextMenuItemDrawer,
disabled && styles.contextMenuItemDisabled,
className
) }
id = { id }
key = { text }
onClick = { onClick }>
{customIcon ? customIcon
: icon && <Icon
className = { styles.contextMenuItemIcon }
size = { 20 }
src = { icon } />}
<span className = { textClassName ?? '' }>{text}</span>
</div>
);
};
export default ContextMenuItem;

View File

@ -1,50 +1,8 @@
// @flow
import { makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import React from 'react';
import { useSelector } from 'react-redux';
import { showOverflowDrawer } from '../../../toolbox/functions.web';
import { Icon } from '../../icons';
export type Action = {
/**
* Label used for accessibility.
*/
accessibilityLabel: string,
/**
* CSS class name used for custom styles.
*/
className?: string,
/**
* Custom icon. If used, the icon prop is ignored.
* Used to allow custom children instead of just the default icons.
*/
customIcon?: React$Node,
/**
* Id of the action container.
*/
id?: string,
/**
* Default icon for action.
*/
icon?: Function,
/**
* Click handler.
*/
onClick?: Function,
/**
* Action text.
*/
text: string
}
import ContextMenuItem, { type Props as Action } from './ContextMenuItem';
type Props = {
@ -59,7 +17,6 @@ type Props = {
children?: React$Node,
};
const useStyles = makeStyles(theme => {
return {
contextMenuItemGroup: {
@ -70,33 +27,6 @@ const useStyles = makeStyles(theme => {
'& + &:not(:empty)': {
borderTop: `1px solid ${theme.palette.ui04}`
}
},
contextMenuItem: {
alignItems: 'center',
cursor: 'pointer',
display: 'flex',
minHeight: '40px',
padding: '10px 16px',
boxSizing: 'border-box',
'& > *:not(:last-child)': {
marginRight: `${theme.spacing(3)}px`
},
'&:hover': {
backgroundColor: theme.palette.ui04
}
},
contextMenuItemDrawer: {
padding: '12px 16px'
},
contextMenuItemIcon: {
'& svg': {
fill: theme.palette.icon01
}
}
};
});
@ -106,28 +36,14 @@ const ContextMenuItemGroup = ({
children
}: Props) => {
const styles = useStyles();
const _overflowDrawer = useSelector(showOverflowDrawer);
return (
<div className = { styles.contextMenuItemGroup }>
{children}
{actions && actions.map(({ accessibilityLabel, className, customIcon, id, icon, onClick, text }) => (
<div
aria-label = { accessibilityLabel }
className = { clsx(styles.contextMenuItem,
_overflowDrawer && styles.contextMenuItemDrawer,
className
) }
id = { id }
key = { text }
onClick = { onClick }>
{customIcon ? customIcon
: icon && <Icon
className = { styles.contextMenuItemIcon }
size = { 20 }
src = { icon } />}
<span>{text}</span>
</div>
{actions && actions.map(actionProps => (
<ContextMenuItem
key = { actionProps.text }
{ ...actionProps } />
))}
</div>
);

View File

@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 4C14 4.85739 13.4605 5.58876 12.7024 5.87317L14.2286 9.94296L14.9455 11.8546C15.0074 11.9292 15.0708 11.9292 15.1098 11.8902L16.5535 10.4465L18.5858 8.41421C18.2239 8.05228 18 7.55228 18 7C18 5.89543 18.8954 5 20 5C21.1046 5 22 5.89543 22 7C22 8.10457 21.1046 9 20 9C19.9441 9 19.8887 8.9977 19.8339 8.9932L19 19C19 20.1046 18.1046 21 17 21H7C5.89543 21 5 20.1046 5 19L4.1661 8.9932C4.11133 8.9977 4.05593 9 4 9C2.89543 9 2 8.10457 2 7C2 5.89543 2.89543 5 4 5C5.10457 5 6 5.89543 6 7C6 7.55228 5.77614 8.05228 5.41421 8.41421L7.44654 10.4465L8.89019 11.8902C8.9775 11.9325 9.03514 11.9063 9.05453 11.8546L9.77139 9.94296L11.2976 5.87317C10.5395 5.58876 10 4.85739 10 4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4ZM6.84027 17L6.44651 12.2749L7.47597 13.3044C7.68795 13.5164 7.94285 13.6805 8.22354 13.7858C9.30949 14.193 10.52 13.6428 10.9272 12.5568L12 9.696L13.0728 12.5568C13.1781 12.8375 13.3422 13.0924 13.5542 13.3044C14.3743 14.1245 15.7039 14.1245 16.524 13.3044L17.5535 12.2749L17.1597 17H6.84027Z"/>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.75 2.5C8.75 3.03587 8.41281 3.49298 7.93902 3.67073L8.89288 6.21435L9.34092 7.40912C9.37965 7.45577 9.41923 7.45577 9.44363 7.43137L10.3459 6.52909L11.6161 5.25888C11.3899 5.03268 11.25 4.72018 11.25 4.375C11.25 3.68464 11.8096 3.125 12.5 3.125C13.1904 3.125 13.75 3.68464 13.75 4.375C13.75 5.06536 13.1904 5.625 12.5 5.625C12.465 5.625 12.4304 5.62356 12.3962 5.62075L11.875 11.875C11.875 12.5654 11.3154 13.125 10.625 13.125H4.375C3.68464 13.125 3.125 12.5654 3.125 11.875L2.60381 5.62075C2.56958 5.62356 2.53496 5.625 2.5 5.625C1.80964 5.625 1.25 5.06536 1.25 4.375C1.25 3.68464 1.80964 3.125 2.5 3.125C3.19036 3.125 3.75 3.68464 3.75 4.375C3.75 4.72018 3.61009 5.03268 3.38388 5.25888L4.65409 6.52909L5.55637 7.43137C5.61094 7.45781 5.64696 7.44144 5.65908 7.40912L6.10712 6.21435L7.06098 3.67073C6.58719 3.49298 6.25 3.03587 6.25 2.5C6.25 1.80964 6.80964 1.25 7.5 1.25C8.19036 1.25 8.75 1.80964 8.75 2.5ZM4.27517 10.625L4.02907 7.67184L4.67248 8.31525C4.80497 8.44773 4.96428 8.55032 5.13971 8.6161C5.81843 8.87063 6.57497 8.52674 6.82949 7.84802L7.5 6.06L8.17051 7.84802C8.23629 8.02345 8.33888 8.18277 8.47136 8.31525C8.98392 8.82781 9.81495 8.82781 10.3275 8.31525L10.9709 7.67184L10.7248 10.625H4.27517Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,11 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 13.078V15C6 16.3999 6.9589 17.5759 8.25572 17.907C8.25195 17.9374 8.25 17.9685 8.25 18V19.4378C6.12171 19.0807 4.5 17.2297 4.5 15C4.5 14.5858 4.16421 14.25 3.75 14.25C3.33579 14.25 3 14.5858 3 15C3 18.0597 5.29027 20.5845 8.25 20.9536V21.75C8.25 22.1642 8.58579 22.5 9 22.5C9.41421 22.5 9.75 22.1642 9.75 21.75V20.9536C10.8412 20.8175 11.8415 20.3884 12.6694 19.7475L15.1986 22.2766C15.4964 22.5744 15.9791 22.5745 16.2768 22.2768C16.5745 21.9791 16.5744 21.4964 16.2766 21.1986L13.7475 18.6694C13.7502 18.6659 13.753 18.6623 13.7557 18.6588L12.6831 17.5861C12.6805 17.5898 12.6779 17.5935 12.6753 17.5972L11.5911 16.513C11.5934 16.5091 11.5957 16.5051 11.598 16.5011L10.4566 15.3596C10.4554 15.3647 10.4541 15.3697 10.4528 15.3748L7.5 12.422V12.403L6 10.903V10.922L2.80143 7.72339C2.50364 7.4256 2.02091 7.42553 1.72322 7.72322C1.42553 8.02091 1.4256 8.50364 1.72339 8.80143L6 13.078ZM7.5 14.578V15C7.5 15.8284 8.17157 16.5 9 16.5C9.1294 16.5 9.25498 16.4836 9.37476 16.4528L7.5 14.578ZM10.513 17.5911C10.2756 17.73 10.0175 17.8372 9.74428 17.907C9.74805 17.9374 9.75 17.9685 9.75 18V19.4378C10.4295 19.3238 11.0573 19.0575 11.5972 18.6753L10.513 17.5911ZM12 14.747L10.5 13.247V10.5C10.5 9.67157 9.82843 9 9 9C8.25144 9 7.63095 9.54832 7.51827 10.2652L6.34845 9.09541C6.85223 8.14635 7.85064 7.5 9 7.5C10.6569 7.5 12 8.84315 12 10.5V14.747ZM13.3623 16.1092L14.5462 17.2932C14.8386 16.5867 15 15.8122 15 15C15 14.5858 14.6642 14.25 14.25 14.25C13.8358 14.25 13.5 14.5858 13.5 15C13.5 15.3828 13.4522 15.7544 13.3623 16.1092Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 4.71869V6C16 6.93329 16.6393 7.71727 17.5038 7.93797C17.5013 7.95829 17.5 7.97899 17.5 8V8.95852C16.0811 8.72048 15 7.4865 15 6C15 5.72386 14.7761 5.5 14.5 5.5C14.2239 5.5 14 5.72386 14 6C14 8.03981 15.5268 9.723 17.5 9.96905V10.5C17.5 10.7761 17.7239 11 18 11C18.2761 11 18.5 10.7761 18.5 10.5V9.96905C19.2275 9.87834 19.8943 9.59227 20.4463 9.16499L22.1324 10.8511C22.3309 11.0496 22.6527 11.0496 22.8512 10.8512C23.0496 10.6527 23.0496 10.3309 22.8511 10.1324L21.165 8.4463C21.1668 8.44393 21.1687 8.44155 21.1705 8.43918L20.4554 7.7241C20.4537 7.72656 20.4519 7.72903 20.4502 7.73149L19.7274 7.00869C19.7289 7.00603 19.7305 7.00338 19.732 7.00072L18.9711 6.23977C18.9702 6.24313 18.9694 6.24649 18.9685 6.24984L17 4.28131V4.26869L16 3.26869V3.28131L13.8676 1.14893C13.6691 0.950402 13.3473 0.950351 13.1488 1.14881C12.9504 1.34727 12.9504 1.6691 13.1489 1.86762L16 4.71869ZM17 5.71869V6C17 6.55228 17.4477 7 18 7C18.0863 7 18.17 6.98908 18.2498 6.96854L17 5.71869ZM19.0087 7.72738C18.8504 7.81999 18.6783 7.89148 18.4962 7.93797C18.4987 7.95829 18.5 7.97899 18.5 8V8.95852C18.953 8.88252 19.3715 8.70502 19.7315 8.45019L19.0087 7.72738ZM20 5.83131L19 4.83131V3C19 2.44772 18.5523 2 18 2C17.501 2 17.0873 2.36555 17.0122 2.84348L16.2323 2.06361C16.5682 1.4309 17.2338 1 18 1C19.1046 1 20 1.89543 20 3V5.83131ZM20.9082 6.73948L21.6975 7.52877C21.8924 7.05778 22 6.54145 22 6C22 5.72386 21.7761 5.5 21.5 5.5C21.2239 5.5 21 5.72386 21 6C21 6.25519 20.9681 6.50294 20.9082 6.73948Z" />
</g>
<defs>
<clipPath id="clip0">
<rect width="24" height="24"/>
</clipPath>
</defs>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 10.8984V12.5C5 13.6666 5.79908 14.6466 6.87977 14.9225C6.87662 14.9479 6.875 14.9737 6.875 15V16.1982C5.10143 15.9006 3.75 14.3581 3.75 12.5C3.75 12.1548 3.47018 11.875 3.125 11.875C2.77982 11.875 2.5 12.1548 2.5 12.5C2.5 15.0498 4.40856 17.1538 6.875 17.4613V18.125C6.875 18.4702 7.15482 18.75 7.5 18.75C7.84518 18.75 8.125 18.4702 8.125 18.125V17.4613C9.03436 17.3479 9.86788 16.9903 10.5579 16.4562L12.6655 18.5638C12.9136 18.812 13.3159 18.8121 13.564 18.564C13.8121 18.3159 13.812 17.9136 13.5638 17.6655L11.4562 15.5579C11.4585 15.5549 11.4608 15.5519 11.4631 15.549L10.5693 14.6551C10.5671 14.6582 10.5649 14.6613 10.5627 14.6644L9.65923 13.7609C9.66117 13.7575 9.6631 13.7542 9.66503 13.7509L8.71384 12.7997C8.7128 12.8039 8.71175 12.8081 8.71067 12.8123L6.25 10.3516V10.3359L5 9.08587V9.10163L2.33453 6.43616C2.08637 6.188 1.68409 6.18794 1.43602 6.43602C1.18794 6.68409 1.188 7.08637 1.43616 7.33453L5 10.8984ZM6.25 12.1484V12.5C6.25 13.1904 6.80964 13.75 7.5 13.75C7.60783 13.75 7.71248 13.7363 7.8123 13.7107L6.25 12.1484ZM8.76086 14.6592C8.56304 14.775 8.34788 14.8643 8.12023 14.9225C8.12338 14.9479 8.125 14.9737 8.125 15V16.1982C8.69123 16.1032 9.21443 15.8813 9.66436 15.5627L8.76086 14.6592ZM10 12.2891L8.75 11.0391V8.75C8.75 8.05964 8.19036 7.5 7.5 7.5C6.8762 7.5 6.35913 7.95693 6.26522 8.55435L5.29038 7.57951C5.71019 6.78863 6.5422 6.25 7.5 6.25C8.88071 6.25 10 7.36929 10 8.75V12.2891ZM11.1352 13.4243L12.1218 14.411C12.3655 13.8222 12.5 13.1768 12.5 12.5C12.5 12.1548 12.2202 11.875 11.875 11.875C11.5298 11.875 11.25 12.1548 11.25 12.5C11.25 12.819 11.2102 13.1287 11.1352 13.4243Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 3.93224V5C13.3333 5.77774 13.8661 6.43106 14.5865 6.61497C14.5844 6.63191 14.5833 6.64916 14.5833 6.66666V7.46543C13.401 7.26706 12.5 6.23874 12.5 4.99999C12.5 4.76988 12.3135 4.58333 12.0833 4.58333C11.8532 4.58333 11.6667 4.76988 11.6667 4.99999C11.6667 6.69984 12.939 8.1025 14.5833 8.30754V8.74999C14.5833 8.98011 14.7699 9.16666 15 9.16666C15.2301 9.16666 15.4167 8.98011 15.4167 8.74999V8.30754C16.0229 8.23194 16.5786 7.99355 17.0386 7.63749L18.4437 9.04256C18.6091 9.20799 18.8773 9.20804 19.0427 9.04265C19.2081 8.87727 19.208 8.60908 19.0426 8.44364L17.6375 7.03857C17.639 7.0366 17.6406 7.03462 17.6421 7.03264L17.0462 6.43674C17.0447 6.4388 17.0433 6.44085 17.0418 6.4429L16.4395 5.84057C16.4408 5.83836 16.4421 5.83614 16.4434 5.83392L15.8092 5.1998C15.8085 5.2026 15.8078 5.2054 15.8071 5.2082L14.1667 3.56775V3.55724L13.3333 2.72391V2.73442L11.5564 0.957435C11.3909 0.791997 11.1227 0.791954 10.9574 0.957339C10.792 1.12272 10.792 1.39091 10.9574 1.55635L13.3333 3.93224ZM14.1667 4.76557V4.99999C14.1667 5.46023 14.5398 5.83333 15 5.83333C15.0719 5.83333 15.1417 5.82422 15.2082 5.80711L14.1667 4.76557ZM15.8406 6.43948C15.7087 6.51666 15.5653 6.57623 15.4135 6.61497C15.4156 6.63191 15.4167 6.64916 15.4167 6.66666V7.46543C15.7942 7.4021 16.143 7.25417 16.4429 7.04182L15.8406 6.43948ZM16.6667 4.85942L15.8333 4.02608V2.49999C15.8333 2.03976 15.4602 1.66666 15 1.66666C14.5841 1.66666 14.2394 1.97128 14.1768 2.36956L13.5269 1.71967C13.8068 1.19241 14.3615 0.833328 15 0.833328C15.9205 0.833328 16.6667 1.57952 16.6667 2.49999V4.85942ZM17.4235 5.61623L18.0812 6.27397C18.2437 5.88148 18.3333 5.45121 18.3333 4.99999C18.3333 4.76988 18.1468 4.58333 17.9167 4.58333C17.6866 4.58333 17.5 4.76988 17.5 4.99999C17.5 5.21265 17.4735 5.41911 17.4235 5.61623Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

4
react/features/base/icons/svg/share-desktop.svg Executable file → Normal file
View File

@ -1,3 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.66671 2.75H18.3334C19.3459 2.75 20.1667 3.57081 20.1667 4.58333V15.5833C20.1667 16.5959 19.3459 17.4167 18.3334 17.4167H15.5834C16.0896 17.4167 16.5 17.8271 16.5 18.3333C16.5 18.8396 16.0896 19.25 15.5834 19.25H6.41671C5.91045 19.25 5.50004 18.8396 5.50004 18.3333C5.50004 17.8271 5.91045 17.4167 6.41671 17.4167H3.66671C2.65419 17.4167 1.83337 16.5959 1.83337 15.5833V4.58333C1.83337 3.57081 2.65419 2.75 3.66671 2.75ZM3.66671 4.58333V15.5833H18.3334V4.58333H3.66671ZM11.9167 8.25C8.16671 8.25 6.41671 9.85417 6.41671 14.6667C8.41671 10.7708 11.9167 11 11.9167 11V12.274C11.9167 12.6941 12.4034 12.9269 12.7305 12.6633L16.017 10.0143C16.2654 9.81413 16.2654 9.43582 16.017 9.23568L12.7305 6.5867C12.4034 6.32307 11.9167 6.55589 11.9167 6.97599V8.25Z" />
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 1.875H12.5C13.1904 1.875 13.75 2.43464 13.75 3.125V10.625C13.75 11.3154 13.1904 11.875 12.5 11.875H10.625C10.9702 11.875 11.25 12.1548 11.25 12.5C11.25 12.8452 10.9702 13.125 10.625 13.125H4.375C4.02982 13.125 3.75 12.8452 3.75 12.5C3.75 12.1548 4.02982 11.875 4.375 11.875H2.5C1.80964 11.875 1.25 11.3154 1.25 10.625V3.125C1.25 2.43464 1.80964 1.875 2.5 1.875ZM2.5 3.125V10.625H12.5V3.125H2.5ZM8.125 5.625C5.56818 5.625 4.375 6.71875 4.375 10C5.73864 7.34375 8.125 7.5 8.125 7.5V8.03605C8.125 8.45615 8.61169 8.68897 8.93877 8.42534L10.767 6.95178C11.0153 6.75164 11.0153 6.37333 10.767 6.17319L8.93877 4.69963C8.61169 4.436 8.125 4.66882 8.125 5.08892V5.625Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 910 B

After

Width:  |  Height:  |  Size: 834 B

View File

@ -1,5 +1,4 @@
/* @flow */
import clsx from 'clsx';
import React, { Component } from 'react';
import { Drawer, JitsiPortal, DialogPortal } from '../../../toolbox/components/web';
@ -60,11 +59,6 @@ type Props = {
*/
position: string,
/**
* Whether the content show have some padding.
*/
paddedContent: ?boolean,
/**
* Whether the popover is visible or not.
*/
@ -364,15 +358,11 @@ class Popover extends Component<Props, State> {
* @returns {ReactElement}
*/
_renderContent() {
const { content, paddedContent } = this.props;
const className = clsx(
'popover popupmenu',
paddedContent && 'padded-content'
);
const { content } = this.props;
return (
<div
className = { className }
className = 'popover'
onKeyDown = { this._onEscKey }>
{ content }
{!isMobileBrowser() && (

View File

@ -1,6 +1,7 @@
/* @flow */
import React, { Component } from 'react';
import { makeStyles } from '@material-ui/core';
import React from 'react';
import { translate } from '../../../i18n';
import { Icon } from '../../../icons';
@ -12,7 +13,7 @@ import { Tooltip } from '../../../tooltip';
type Props = {
/**
* Additional CSS class names to set on the icon container.
* Additional CSS class name.
*/
className: string,
@ -59,42 +60,35 @@ type Props = {
tooltipPosition: string
};
const useStyles = makeStyles(() => {
return {
indicator: {
width: '20px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}
};
});
/**
* React {@code Component} for showing an icon with a tooltip.
*
* @augments Component
*/
class BaseIndicator extends Component<Props> {
/**
* Default values for {@code BaseIndicator} component's properties.
*
* @static
*/
static defaultProps = {
className: '',
id: '',
tooltipPosition: 'top'
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
className,
const BaseIndicator = ({
className = '',
icon,
iconClassName,
iconId,
iconSize,
id,
id = '',
t,
tooltipKey,
tooltipPosition
} = this.props;
const iconContainerClassName = `indicator-icon-container ${className}`;
tooltipPosition = 'top'
}: Props) => {
const styles = useStyles();
const style = {};
if (iconSize) {
@ -102,12 +96,12 @@ class BaseIndicator extends Component<Props> {
}
return (
<div className = 'indicator-container'>
<div className = { styles.indicator }>
<Tooltip
content = { t(tooltipKey) }
position = { tooltipPosition }>
<span
className = { iconContainerClassName }
className = { className }
id = { id }>
<Icon
className = { iconClassName }
@ -118,7 +112,6 @@ class BaseIndicator extends Component<Props> {
</Tooltip>
</div>
);
}
}
};
export default translate(BaseIndicator);

View File

@ -1,5 +1,7 @@
// @flow
import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React from 'react';
import type { Dispatch } from 'redux';
@ -30,24 +32,21 @@ const QUALITY_TO_WIDTH: Array<Object> = [
{
colorClass: 'status-high',
percent: INDICATOR_DISPLAY_THRESHOLD,
tip: 'connectionindicator.quality.good',
width: '100%'
tip: 'connectionindicator.quality.good'
},
// 2 bars
{
colorClass: 'status-med',
percent: 10,
tip: 'connectionindicator.quality.nonoptimal',
width: '66%'
tip: 'connectionindicator.quality.nonoptimal'
},
// 1 bar
{
colorClass: 'status-low',
percent: 0,
tip: 'connectionindicator.quality.poor',
width: '33%'
tip: 'connectionindicator.quality.poor'
}
// Note: we never show 0 bars as long as there is a connection.
@ -85,6 +84,11 @@ type Props = AbstractProps & {
*/
audioSsrc: number,
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* The Redux dispatch function.
*/
@ -122,6 +126,52 @@ type State = AbstractState & {
popoverVisible: boolean
}
const styles = theme => {
return {
container: {
display: 'inline-block'
},
hidden: {
display: 'none'
},
icon: {
padding: '6px',
borderRadius: '4px',
'&.status-high': {
backgroundColor: theme.palette.success01
},
'&.status-med': {
backgroundColor: theme.palette.warning01
},
'&.status-low': {
backgroundColor: theme.palette.iconError
},
'&.status-disabled': {
background: 'transparent'
},
'&.status-lost': {
backgroundColor: theme.palette.ui05
},
'&.status-other': {
backgroundColor: theme.palette.action01
}
},
inactiveIcon: {
padding: 0,
borderRadius: '50%'
}
};
};
/**
* Implements a React {@link Component} which displays the current connection
* quality percentage and has a popover to show more detailed connection stats.
@ -154,9 +204,8 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
* @returns {ReactElement}
*/
render() {
const { enableStatsDisplay, participantId, statsPopoverPosition } = this.props;
const { enableStatsDisplay, participantId, statsPopoverPosition, classes } = this.props;
const visibilityClass = this._getVisibilityClass();
const rootClassNames = `indicator-container ${visibilityClass}`;
if (this.props._popoverDisabled) {
return this._renderIndicator();
@ -164,7 +213,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
return (
<Popover
className = { rootClassNames }
className = { clsx(classes.container, visibilityClass) }
content = { <ConnectionIndicatorContent
inheritedStats = { this.state.stats }
participantId = { participantId } /> }
@ -173,7 +222,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
noPaddingContent = { true }
onPopoverClose = { this._onHidePopover }
onPopoverOpen = { this._onShowPopover }
paddedContent = { true }
position = { statsPopoverPosition }
visible = { this.state.popoverVisible }>
{ this._renderIndicator() }
@ -231,13 +279,13 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
* @returns {string}
*/
_getVisibilityClass() {
const { _connectionStatus } = this.props;
const { _connectionStatus, classes } = this.props;
return this.state.showIndicator
|| this.props.alwaysVisible
|| _connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED
|| _connectionStatus === JitsiParticipantConnectionStatus.INACTIVE
? 'show-connection-indicator' : 'hide-connection-indicator';
? '' : classes.hidden;
}
_onHidePopover: () => void;
@ -259,6 +307,8 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
* @returns {ReactElement}
*/
_renderIcon() {
const colorClass = this._getConnectionColorClass();
if (this.props._connectionStatus === JitsiParticipantConnectionStatus.INACTIVE) {
if (this.props._connectionIndicatorInactiveDisabled) {
return null;
@ -267,14 +317,13 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
return (
<span className = 'connection_ninja'>
<Icon
className = 'icon-ninja'
size = '1.5em'
className = { clsx(this.props.classes.icon, this.props.classes.inactiveIcon, colorClass) }
size = { 24 }
src = { IconConnectionInactive } />
</span>
);
}
let iconWidth;
let emptyIconWrapperClassName = 'connection_empty';
if (this.props._connectionStatus
@ -283,34 +332,16 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
// emptyIconWrapperClassName is used by the torture tests to
// identify lost connection status handling.
emptyIconWrapperClassName = 'connection_lost';
iconWidth = '0%';
} else if (typeof this.state.stats.percent === 'undefined') {
iconWidth = '100%';
} else {
const { percent } = this.state.stats;
iconWidth = this._getDisplayConfiguration(percent).width;
}
return [
<span
className = { emptyIconWrapperClassName }
key = 'icon-empty'>
return (
<span className = { emptyIconWrapperClassName }>
<Icon
className = 'icon-gsm-bars'
size = '1em'
src = { IconConnectionActive } />
</span>,
<span
className = 'connection_full'
key = 'icon-full'
style = {{ width: iconWidth }}>
<Icon
className = 'icon-gsm-bars'
size = '1em'
className = { clsx(this.props.classes.icon, colorClass) }
size = { 12 }
src = { IconConnectionActive } />
</span>
];
);
}
_onShowPopover: () => void;
@ -332,20 +363,11 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
* @returns {ReactElement}
*/
_renderIndicator() {
const colorClass = this._getConnectionColorClass();
const indicatorContainerClassNames
= `connection-indicator indicator ${colorClass}`;
return (
<div className = 'popover-trigger'>
<div
className = { indicatorContainerClassNames }
style = {{ fontSize: this.props.iconSize }}>
<div className = 'connection indicatoricon'>
{this._renderIcon()}
</div>
</div>
</div>
);
}
}
@ -369,4 +391,5 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
_connectionStatus: participant?.connectionStatus
};
}
export default translate(connect(_mapStateToProps)(ConnectionIndicator));
export default translate(connect(_mapStateToProps)(
withStyles(styles)(ConnectionIndicator)));

View File

@ -1,8 +1,10 @@
/* @flow */
import { withStyles } from '@material-ui/styles';
import React, { Component } from 'react';
import { isMobileBrowser } from '../../../features/base/environment/utils';
import ContextMenu from '../../base/components/context-menu/ContextMenu';
import { translate } from '../../base/i18n';
/**
@ -40,6 +42,11 @@ type Props = {
*/
bridgeCount: number,
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* Audio/video codecs in use for the connection.
*/
@ -163,6 +170,20 @@ function onClick(event) {
event.stopPropagation();
}
const styles = theme => {
return {
contextMenu: {
position: 'relative',
marginTop: 0,
right: 'auto',
padding: `${theme.spacing(2)}px ${theme.spacing(1)}px`,
marginLeft: '4px',
marginRight: '4px',
marginBottom: '4px'
}
};
};
/**
* React {@code Component} for displaying connection statistics.
*
@ -176,10 +197,14 @@ class ConnectionStatsTable extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { isLocalVideo, enableSaveLogs, disableShowMoreStats } = this.props;
const { isLocalVideo, enableSaveLogs, disableShowMoreStats, classes } = this.props;
const className = isMobileBrowser() ? 'connection-info connection-info__mobile' : 'connection-info';
return (
<ContextMenu
className = { classes.contextMenu }
hidden = { false }
inDrawer = { true }>
<div
className = { className }
onClick = { onClick }>
@ -190,6 +215,7 @@ class ConnectionStatsTable extends Component<Props> {
</div>
{ this.props.shouldShowMore ? this._renderAdditionalStats() : null }
</div>
</ContextMenu>
);
}
@ -839,4 +865,4 @@ function getStringFromArray(array) {
return res;
}
export default translate(ConnectionStatsTable);
export default translate(withStyles(styles)(ConnectionStatsTable));

View File

@ -1,5 +1,6 @@
/* @flow */
import { withStyles } from '@material-ui/styles';
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
@ -10,6 +11,8 @@ import {
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { updateSettings } from '../../../base/settings';
import { Tooltip } from '../../../base/tooltip';
import { getIndicatorsTooltipPosition } from '../../../filmstrip/functions.web';
import { appendSuffix } from '../../functions';
/**
@ -33,6 +36,11 @@ type Props = {
*/
allowEditing: boolean,
/**
* The current layout of the filmstrip.
*/
currentLayout: string,
/**
* Invoked to update the participant's display name.
*/
@ -43,6 +51,11 @@ type Props = {
*/
displayNameSuffix: string,
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* The ID attribute to add to the component. Useful for global querying for
* the component by legacy components and torture tests.
@ -76,6 +89,30 @@ type State = {
isEditing: boolean
};
const styles = theme => {
return {
displayName: {
...theme.typography.labelBold,
lineHeight: `${theme.typography.labelBold.lineHeight}px`,
color: theme.palette.text01,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
},
editDisplayName: {
outline: 'none',
border: 'none',
background: 'none',
boxShadow: 'none',
padding: 0,
...theme.typography.labelBold,
lineHeight: `${theme.typography.labelBold.lineHeight}px`,
color: theme.palette.text01
}
};
};
/**
* React {@code Component} for displaying and editing a participant's name.
*
@ -146,7 +183,9 @@ class DisplayName extends Component<Props, State> {
const {
_nameToDisplay,
allowEditing,
currentLayout,
displayNameSuffix,
classes,
elementID,
t
} = this.props;
@ -155,10 +194,11 @@ class DisplayName extends Component<Props, State> {
return (
<input
autoFocus = { true }
className = 'editdisplayname'
className = { classes.editDisplayName }
id = 'editDisplayName'
onBlur = { this._onSubmit }
onChange = { this._onChange }
onClick = { this._onClick }
onKeyDown = { this._onKeyDown }
placeholder = { t('defaultNickname') }
ref = { this._setNameInputRef }
@ -169,15 +209,30 @@ class DisplayName extends Component<Props, State> {
}
return (
<Tooltip
content = { appendSuffix(_nameToDisplay, displayNameSuffix) }
position = { getIndicatorsTooltipPosition(currentLayout) }>
<span
className = 'displayname'
className = { `displayname ${classes.displayName}` }
id = { elementID }
onClick = { this._onStartEditing }>
{ appendSuffix(_nameToDisplay, displayNameSuffix) }
</span>
</Tooltip>
);
}
/**
* Stop click event propagation.
*
* @param {MouseEvent} e - The click event.
* @private
* @returns {void}
*/
_onClick(e) {
e.stopPropagation();
}
_onChange: () => void;
/**
@ -215,11 +270,13 @@ class DisplayName extends Component<Props, State> {
* Updates the component to display an editable input field and sets the
* initial value to the current display name.
*
* @param {MouseEvent} e - The click event.
* @private
* @returns {void}
*/
_onStartEditing() {
_onStartEditing(e) {
if (this.props.allowEditing) {
e.stopPropagation();
this.setState({
isEditing: true,
editDisplayNameValue: this.props._configuredDisplayName
@ -292,4 +349,4 @@ function _mapStateToProps(state, ownProps) {
};
}
export default translate(connect(_mapStateToProps)(DisplayName));
export default translate(connect(_mapStateToProps)(withStyles(styles)(DisplayName)));

View File

@ -1,8 +1,8 @@
/* @flow */
import React, { Component } from 'react';
import React from 'react';
import { IconMicDisabled } from '../../../base/icons';
import { IconMicrophoneEmptySlash } from '../../../base/icons';
import { BaseIndicator } from '../../../base/react';
/**
@ -19,26 +19,16 @@ type Props = {
/**
* React {@code Component} for showing an audio muted icon with a tooltip.
*
* @augments Component
* @returns {Component}
*/
class AudioMutedIndicator extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
const AudioMutedIndicator = ({ tooltipPosition }: Props) => (
<BaseIndicator
className = 'audioMuted toolbar-icon'
icon = { IconMicDisabled }
icon = { IconMicrophoneEmptySlash }
iconId = 'mic-disabled'
iconSize = { 13 }
iconSize = { 15 }
id = 'audioMuted'
tooltipKey = 'videothumbnail.mute'
tooltipPosition = { this.props.tooltipPosition } />
tooltipPosition = { tooltipPosition } />
);
}
}
export default AudioMutedIndicator;

View File

@ -1,51 +0,0 @@
/* @flow */
import React, { Component } from 'react';
import { IconDominantSpeaker } from '../../../base/icons';
import { BaseIndicator } from '../../../base/react';
/**
* The type of the React {@code Component} props of
* {@link DominantSpeakerIndicator}.
*/
type Props = {
/**
* The font-size for the icon.
*/
iconSize: number,
/**
* From which side of the indicator the tooltip should appear from.
*/
tooltipPosition: string
};
/**
* Thumbnail badge showing that the participant is the dominant speaker in
* the conference.
*
* @augments Component
*/
class DominantSpeakerIndicator extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<BaseIndicator
className = 'indicator show-inline'
icon = { IconDominantSpeaker }
iconClassName = 'indicatoricon'
iconSize = { `${this.props.iconSize}px` }
id = 'dominantspeakerindicator'
tooltipKey = 'speaker'
tooltipPosition = { this.props.tooltipPosition } />
);
}
}
export default DominantSpeakerIndicator;

View File

@ -1,8 +1,8 @@
/* @flow */
import React, { Component } from 'react';
import React from 'react';
import { IconModerator } from '../../../base/icons';
import { IconCrown } from '../../../base/icons';
import { BaseIndicator } from '../../../base/react';
/**
@ -19,27 +19,14 @@ type Props = {
/**
* React {@code Component} for showing a moderator icon with a tooltip.
*
* @augments Component
* @returns {Component}
*/
class ModeratorIndicator extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div className = 'moderator-icon right'>
const ModeratorIndicator = ({ tooltipPosition }: Props) => (
<BaseIndicator
className = 'focusindicator toolbar-icon'
icon = { IconModerator }
iconSize = { 13 }
icon = { IconCrown }
iconSize = { 15 }
tooltipKey = 'videothumbnail.moderator'
tooltipPosition = { this.props.tooltipPosition } />
</div>
tooltipPosition = { tooltipPosition } />
);
}
}
export default ModeratorIndicator;

View File

@ -1,53 +1,75 @@
/* @flow */
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import { useSelector } from 'react-redux';
import { IconRaisedHand } from '../../../base/icons';
import { getParticipantById, hasRaisedHand } from '../../../base/participants';
import { BaseIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import AbstractRaisedHandIndicator, {
type Props as AbstractProps,
_mapStateToProps
} from '../AbstractRaisedHandIndicator';
/**
* The type of the React {@code Component} props of {@link RaisedHandIndicator}.
*/
type Props = AbstractProps & {
type Props = {
/**
* The font-size for the icon.
*/
iconSize: number,
/**
* The participant id who we want to render the raised hand indicator
* for.
*/
participantId: string,
/**
* From which side of the indicator the tooltip should appear from.
*/
tooltipPosition: string
};
const useStyles = makeStyles(theme => {
return {
raisedHandIndicator: {
backgroundColor: theme.palette.warning01,
padding: '2px',
zIndex: 3,
display: 'inline-block',
borderRadius: '4px',
boxSizing: 'border-box'
}
};
});
/**
* Thumbnail badge showing that the participant would like to speak.
*
* @augments Component
* @returns {ReactElement}
*/
class RaisedHandIndicator extends AbstractRaisedHandIndicator<Props> {
/**
* Renders the platform specific indicator element.
*
* @returns {React$Element<*>}
*/
_renderIndicator() {
return (
<BaseIndicator
className = 'raisehandindicator indicator show-inline'
icon = { IconRaisedHand }
iconClassName = 'indicatoricon'
iconSize = { `${this.props.iconSize}px` }
tooltipKey = 'raisedHand'
tooltipPosition = { this.props.tooltipPosition } />
);
}
const RaisedHandIndicator = ({
iconSize,
participantId,
tooltipPosition
}: Props) => {
const _raisedHand = hasRaisedHand(useSelector(state =>
getParticipantById(state, participantId)));
const styles = useStyles();
if (!_raisedHand) {
return null;
}
export default connect(_mapStateToProps)(RaisedHandIndicator);
return (
<div className = { styles.raisedHandIndicator }>
<BaseIndicator
icon = { IconRaisedHand }
iconSize = { `${iconSize}px` }
tooltipKey = 'raisedHand'
tooltipPosition = { tooltipPosition } />
</div>
);
};
export default RaisedHandIndicator;

View File

@ -23,10 +23,9 @@ type Props = {
export default function ScreenShareIndicator(props: Props) {
return (
<BaseIndicator
className = 'screenShare toolbar-icon'
icon = { IconShareDesktop }
iconId = 'share-desktop'
iconSize = { 13 }
iconSize = { 15 }
tooltipKey = 'videothumbnail.videomute'
tooltipPosition = { props.tooltipPosition } />
);

View File

@ -6,12 +6,12 @@ import { MEDIA_TYPE } from '../../../base/media';
import { getParticipantByIdOrUndefined, PARTICIPANT_ROLE } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { getTrackByMediaTypeAndParticipant, isLocalTrackMuted, isRemoteTrackMuted } from '../../../base/tracks';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { getCurrentLayout } from '../../../video-layout';
import { getIndicatorsTooltipPosition } from '../../functions.web';
import AudioMutedIndicator from './AudioMutedIndicator';
import ModeratorIndicator from './ModeratorIndicator';
import ScreenShareIndicator from './ScreenShareIndicator';
import VideoMutedIndicator from './VideoMutedIndicator';
declare var interfaceConfig: Object;
@ -40,11 +40,6 @@ type Props = {
*/
_showScreenShareIndicator: Boolean,
/**
* Indicates if the video muted indicator should be visible or not.
*/
_showVideoMutedIndicator: Boolean,
/**
* The ID of the participant for which the status bar is rendered.
*/
@ -68,29 +63,16 @@ class StatusIndicators extends Component<Props> {
_currentLayout,
_showAudioMutedIndicator,
_showModeratorIndicator,
_showScreenShareIndicator,
_showVideoMutedIndicator
_showScreenShareIndicator
} = this.props;
let tooltipPosition;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
tooltipPosition = 'right';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
tooltipPosition = 'left';
break;
default:
tooltipPosition = 'top';
}
const tooltipPosition = getIndicatorsTooltipPosition(_currentLayout);
return (
<div>
{ _showAudioMutedIndicator ? <AudioMutedIndicator tooltipPosition = { tooltipPosition } /> : null }
{ _showScreenShareIndicator ? <ScreenShareIndicator tooltipPosition = { tooltipPosition } /> : null }
{ _showVideoMutedIndicator ? <VideoMutedIndicator tooltipPosition = { tooltipPosition } /> : null }
{ _showModeratorIndicator ? <ModeratorIndicator tooltipPosition = { tooltipPosition } /> : null }
</div>
<>
{ _showAudioMutedIndicator && <AudioMutedIndicator tooltipPosition = { tooltipPosition } /> }
{ _showModeratorIndicator && <ModeratorIndicator tooltipPosition = { tooltipPosition } />}
{ _showScreenShareIndicator && <ScreenShareIndicator tooltipPosition = { tooltipPosition } /> }
</>
);
}
}
@ -108,24 +90,21 @@ class StatusIndicators extends Component<Props> {
* }}
*/
function _mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
const { participantID, audio, moderator, screenshare } = ownProps;
// Only the local participant won't have id for the time when the conference is not yet joined.
const participant = getParticipantByIdOrUndefined(state, participantID);
const tracks = state['features/base/tracks'];
let isVideoMuted = true;
let isAudioMuted = true;
let isScreenSharing = false;
if (participant?.local) {
isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
isAudioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
} else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
const track = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
isScreenSharing = track?.videoType === 'desktop';
isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, participantID);
isAudioMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID);
}
@ -133,11 +112,10 @@ function _mapStateToProps(state, ownProps) {
return {
_currentLayout: getCurrentLayout(state),
_showAudioMutedIndicator: isAudioMuted,
_showAudioMutedIndicator: isAudioMuted && audio,
_showModeratorIndicator:
!disableModeratorIndicator && participant && participant.role === PARTICIPANT_ROLE.MODERATOR,
_showScreenShareIndicator: isScreenSharing,
_showVideoMutedIndicator: isVideoMuted
!disableModeratorIndicator && participant && participant.role === PARTICIPANT_ROLE.MODERATOR && moderator,
_showScreenShareIndicator: isScreenSharing && screenshare
};
}

View File

@ -1,17 +1,15 @@
// @flow
import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React, { Component } from 'react';
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
import { AudioLevelIndicator } from '../../../audio-level-indicator';
import { Avatar } from '../../../base/avatar';
import { isNameReadOnly } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
import {
getParticipantByIdOrUndefined,
getParticipantCount,
pinParticipant
} from '../../../base/participants';
import { connect } from '../../../base/redux';
@ -23,23 +21,19 @@ import {
getTrackByMediaTypeAndParticipant,
updateLastTrackVideoMediaEvent
} from '../../../base/tracks';
import { ConnectionIndicator } from '../../../connection-indicator';
import { DisplayName } from '../../../display-name';
import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
import { PresenceLabel } from '../../../presence-status';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
import { setVolume } from '../../actions.web';
import {
DISPLAY_MODE_TO_CLASS_NAME,
DISPLAY_VIDEO,
DISPLAY_VIDEO_WITH_NAME,
VIDEO_TEST_EVENTS,
SHOW_TOOLBAR_CONTEXT_MENU_AFTER
} from '../../constants';
import { isVideoPlayable, computeDisplayMode } from '../../functions';
import { isVideoPlayable, computeDisplayModeFromInput, getDisplayModeInput } from '../../functions';
const JitsiTrackEvents = JitsiMeetJS.events.track;
import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
import ThumbnailTopIndicators from './ThumbnailTopIndicators';
declare var interfaceConfig: Object;
@ -48,11 +42,6 @@ declare var interfaceConfig: Object;
*/
export type State = {|
/**
* The current audio level value for the Thumbnail.
*/
audioLevel: number,
/**
* Indicates that the canplay event has been received.
*/
@ -63,15 +52,15 @@ export type State = {|
*/
displayMode: number,
/**
* Indicates whether the thumbnail is hovered or not.
*/
isHovered: boolean,
/**
* Whether popover is visible or not.
*/
popoverVisible: boolean
popoverVisible: boolean,
/**
* Indicates whether the thumbnail is hovered or not.
*/
isHovered: boolean
|};
/**
@ -79,36 +68,16 @@ export type State = {|
*/
export type Props = {|
/**
* If the display name is editable or not.
*/
_allowEditing: boolean,
/**
* The audio track related to the participant.
*/
_audioTrack: ?Object,
/**
* Disable/enable the auto hide functionality for the connection indicator.
*/
_connectionIndicatorAutoHideEnabled: boolean,
/**
* Disable/enable the connection indicator.
*/
_connectionIndicatorDisabled: boolean,
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* The default display name for the local participant.
*/
_defaultLocalDisplayName: string,
/**
* Indicates whether the local video flip feature is disabled or not.
*/
@ -119,26 +88,21 @@ export type Props = {|
*/
_disableTileEnlargement: boolean,
/**
* The display mode of the thumbnail.
*/
_displayMode: number,
/**
* The height of the Thumbnail.
*/
_height: number,
/**
* The aspect ratio of the Thumbnail in percents.
*/
_heightToWidthPercent: number,
/**
* Indicates whether the thumbnail should be hidden or not.
*/
_isHidden: boolean,
/**
* Whether or not there is a pinned participant.
*/
_isAnyParticipantPinned: boolean,
/**
* Indicates whether audio only mode is enabled.
*/
@ -179,11 +143,6 @@ export type Props = {|
*/
_isTestModeEnabled: boolean,
/**
* The size of the icon of indicators.
*/
_indicatorIconSize: number,
/**
* The current local video flip setting.
*/
@ -194,26 +153,11 @@ export type Props = {|
*/
_participant: Object,
/**
* True if there are more than 2 participants in the call.
*/
_participantCountMoreThan2: boolean,
/**
* Indicates whether the "start silent" mode is enabled.
*/
_startSilent: Boolean,
/**
* The video track that will be displayed in the thumbnail.
*/
_videoTrack: ?Object,
/**
* The volume level for the thumbnail.
*/
_volume?: ?number,
/**
* The width of the thumbnail.
*/
@ -224,6 +168,11 @@ export type Props = {|
*/
dispatch: Function,
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
*/
@ -240,17 +189,74 @@ export type Props = {|
style?: ?Object
|};
/**
* Click handler for the display name container.
*
* @param {SyntheticEvent} event - The click event.
* @returns {void}
*/
function onClick(event) {
// If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
// needs to be stopped.
event.stopPropagation();
const defaultStyles = theme => {
return {
indicatorsContainer: {
position: 'absolute',
padding: `${theme.spacing(1)}px`,
zIndex: 10,
width: '100%',
boxSizing: 'border-box',
display: 'flex',
left: 0,
'&.tile-view-mode': {
padding: `${theme.spacing(2)}px`
}
},
indicatorsTopContainer: {
top: 0,
justifyContent: 'space-between'
},
indicatorsBottomContainer: {
bottom: 0
},
indicatorsBackground: {
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
maxWidth: '100%',
overflow: 'hidden',
'&:not(:empty)': {
padding: '2px'
},
'& > *:not(:last-child)': {
marginRight: '4px'
},
'&:not(.top-indicators) > *:last-child': {
marginRight: '6px'
}
},
containerBackground: {
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
borderRadius: '4px',
backgroundColor: theme.palette.ui02
},
activeSpeaker: {
'& .active-speaker-indicator': {
boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`,
position: 'absolute',
width: '100%',
height: '100%',
zIndex: '9',
borderRadius: '4px'
}
}
};
};
/**
* Implements a thumbnail.
@ -263,11 +269,6 @@ class Thumbnail extends Component<Props, State> {
*/
timeoutHandle: Object;
/**
* Reference to local or remote Video Menu trigger button instance.
*/
videoMenuTriggerRef: Object;
/**
* Timeout used to detect double tapping.
* It is active while user has tapped once.
@ -284,26 +285,21 @@ class Thumbnail extends Component<Props, State> {
super(props);
const state = {
audioLevel: 0,
canPlayEventReceived: false,
isHovered: false,
displayMode: DISPLAY_VIDEO,
popoverVisible: false
popoverVisible: false,
isHovered: false
};
this.state = {
...state,
displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, state)),
popoverVisible: false
displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, state))
};
this.timeoutHandle = null;
this.videoMenuTriggerRef = null;
this._clearDoubleClickTimeout = this._clearDoubleClickTimeout.bind(this);
this._updateAudioLevel = this._updateAudioLevel.bind(this);
this._onCanPlay = this._onCanPlay.bind(this);
this._onClick = this._onClick.bind(this);
this._onVolumeChange = this._onVolumeChange.bind(this);
this._onMouseEnter = this._onMouseEnter.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
this._onTestingEvent = this._onTestingEvent.bind(this);
@ -321,7 +317,6 @@ class Thumbnail extends Component<Props, State> {
* @returns {void}
*/
componentDidMount() {
this._listenForAudioUpdates();
this._onDisplayModeChanged();
}
@ -333,12 +328,6 @@ class Thumbnail extends Component<Props, State> {
* @returns {void}
*/
componentDidUpdate(prevProps: Props, prevState: State) {
if (prevProps._audioTrack !== this.props._audioTrack) {
this._stopListeningForAudioUpdates(prevProps._audioTrack);
this._listenForAudioUpdates();
this._updateAudioLevel(0);
}
if (prevState.displayMode !== this.state.displayMode) {
this._onDisplayModeChanged();
}
@ -350,7 +339,7 @@ class Thumbnail extends Component<Props, State> {
* @returns {void}
*/
_onDisplayModeChanged() {
const input = Thumbnail.getDisplayModeInput(this.props, this.state);
const input = getDisplayModeInput(this.props, this.state);
this._maybeSendScreenSharingIssueEvents(input);
}
@ -370,7 +359,7 @@ class Thumbnail extends Component<Props, State> {
const { displayMode } = this.state;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
if (![ DISPLAY_VIDEO, DISPLAY_VIDEO_WITH_NAME ].includes(displayMode)
if (!(DISPLAY_VIDEO === displayMode)
&& tileViewActive
&& _isScreenSharing
&& !_isAudioOnly) {
@ -395,11 +384,11 @@ class Thumbnail extends Component<Props, State> {
return {
...newState,
displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, newState))
displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, newState))
};
}
const newDisplayMode = computeDisplayMode(Thumbnail.getDisplayModeInput(props, prevState));
const newDisplayMode = computeDisplayModeFromInput(getDisplayModeInput(props, prevState));
if (newDisplayMode !== prevState.displayMode) {
return {
@ -411,51 +400,6 @@ class Thumbnail extends Component<Props, State> {
return null;
}
/**
* Extracts information for props and state needed to compute the display mode.
*
* @param {Props} props - The component's props.
* @param {State} state - The component's state.
* @returns {Object}
*/
static getDisplayModeInput(props: Props, state: State) {
const {
_currentLayout,
_isAudioOnly,
_isCurrentlyOnLargeVideo,
_isScreenSharing,
_isVideoPlayable,
_participant,
_videoTrack
} = props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
const { canPlayEventReceived, isHovered } = state;
return {
isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
isHovered,
isAudioOnly: _isAudioOnly,
tileViewActive,
isVideoPlayable: _isVideoPlayable,
connectionStatus: _participant?.connectionStatus,
canPlayEventReceived,
videoStream: Boolean(_videoTrack),
isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
isScreenSharing: _isScreenSharing,
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
};
}
/**
* Unsubscribe from audio level updates.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._stopListeningForAudioUpdates(this.props._audioTrack);
}
_clearDoubleClickTimeout: () => void;
/**
@ -468,53 +412,6 @@ class Thumbnail extends Component<Props, State> {
this._firstTap = undefined;
}
/**
* Starts listening for audio level updates from the library.
*
* @private
* @returns {void}
*/
_listenForAudioUpdates() {
const { _audioTrack } = this.props;
if (_audioTrack) {
const { jitsiTrack } = _audioTrack;
jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
}
}
/**
* Stops listening to further updates from the passed track.
*
* @param {Object} audioTrack - The track.
* @private
* @returns {void}
*/
_stopListeningForAudioUpdates(audioTrack) {
if (audioTrack) {
const { jitsiTrack } = audioTrack;
jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
}
}
_updateAudioLevel: (number) => void;
/**
* Updates the internal state of the last know audio level. The level should
* be between 0 and 1, as the level will be used as a percentage out of 1.
*
* @param {number} audioLevel - The new audio level for the track.
* @private
* @returns {void}
*/
_updateAudioLevel(audioLevel) {
this.setState({
audioLevel
});
}
_showPopover: () => void;
/**
@ -549,7 +446,6 @@ class Thumbnail extends Component<Props, State> {
* @returns {Object} - The styles for the thumbnail.
*/
_getStyles(): Object {
const { canPlayEventReceived } = this.state;
const {
_currentLayout,
@ -575,7 +471,7 @@ class Thumbnail extends Component<Props, State> {
video: {}
};
const avatarSize = _height / 2;
const avatarSize = Math.min(_height / 2, _width - 30);
let { left } = style || {};
if (typeof left === 'number' && horizontalOffset) {
@ -730,67 +626,6 @@ class Thumbnail extends Component<Props, State> {
);
}
/**
* Renders the top indicators of the thumbnail.
*
* @returns {Component}
*/
_renderTopIndicators() {
const {
_connectionIndicatorAutoHideEnabled,
_connectionIndicatorDisabled,
_currentLayout,
_isDominantSpeakerDisabled,
_indicatorIconSize: iconSize,
_participant,
_participantCountMoreThan2
} = this.props;
const { isHovered } = this.state;
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
const { id, dominantSpeaker = false } = _participant;
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
let statsPopoverPosition, tooltipPosition;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
statsPopoverPosition = 'right-start';
tooltipPosition = 'right';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
statsPopoverPosition = 'left-start';
tooltipPosition = 'left';
break;
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
statsPopoverPosition = 'top';
tooltipPosition = 'top';
break;
default:
statsPopoverPosition = 'auto';
tooltipPosition = 'top';
}
return (
<div>
{ !_connectionIndicatorDisabled
&& <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { true }
iconSize = { iconSize }
participantId = { id }
statsPopoverPosition = { statsPopoverPosition } />
}
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { id }
tooltipPosition = { tooltipPosition } />
{ showDominantSpeaker && _participantCountMoreThan2
&& <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
}
</div>);
}
/**
* Renders the avatar.
*
@ -820,115 +655,31 @@ class Thumbnail extends Component<Props, State> {
_getContainerClassName() {
let className = 'videocontainer';
const { displayMode } = this.state;
const { _isAudioOnly, _isDominantSpeakerDisabled, _isHidden, _participant } = this.props;
const isRemoteParticipant = !_participant?.local && !_participant?.isFakeParticipant;
const {
_isDominantSpeakerDisabled,
_participant,
_currentLayout,
_isAnyParticipantPinned,
classes
} = this.props;
className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`;
if (_participant?.pinned) {
className += ' videoContainerFocused';
}
if (_currentLayout === LAYOUTS.TILE_VIEW) {
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
className += ' active-speaker';
className += ` ${classes.activeSpeaker} dominant-speaker`;
}
if (_isHidden) {
className += ' hidden';
} else if (_isAnyParticipantPinned) {
if (_participant?.pinned) {
className += ` videoContainerFocused ${classes.activeSpeaker}`;
}
if (isRemoteParticipant && _isAudioOnly) {
className += ' audio-only';
} else if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
className += ` ${classes.activeSpeaker} dominant-speaker`;
}
return className;
}
/**
* Renders the local participant's thumbnail.
*
* @returns {ReactElement}
*/
_renderLocalParticipant() {
const {
_allowEditing,
_defaultLocalDisplayName,
_disableLocalVideoFlip,
_isMobile,
_isMobilePortrait,
_isScreenSharing,
_localFlipX,
_participant,
_videoTrack
} = this.props;
const { id } = _participant || {};
const { audioLevel } = this.state;
const styles = this._getStyles();
let containerClassName = this._getContainerClassName();
const videoTrackClassName
= !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
if (_isMobilePortrait) {
styles.thumbnail.height = styles.thumbnail.width;
containerClassName = `${containerClassName} self-view-mobile-portrait`;
}
return (
<span
className = { containerClassName }
id = 'localVideoContainer'
{ ...(_isMobile
? {
onTouchEnd: this._onTouchEnd,
onTouchMove: this._onTouchMove,
onTouchStart: this._onTouchStart
}
: {
onClick: this._onClick,
onMouseEnter: this._onMouseEnter,
onMouseLeave: this._onMouseLeave
}
) }
style = { styles.thumbnail }>
<div className = 'videocontainer__background' />
<span id = 'localVideoWrapper'>
<VideoTrack
className = { videoTrackClassName }
id = 'localVideo_container'
style = { styles.video }
videoTrack = { _videoTrack } />
</span>
<div className = 'videocontainer__toolbar'>
<StatusIndicators participantID = { id } />
<div
className = 'videocontainer__participant-name'
onClick = { onClick }>
<DisplayName
allowEditing = { _allowEditing }
displayNameSuffix = { _defaultLocalDisplayName }
elementID = 'localDisplayName'
participantID = { id } />
</div>
</div>
<div className = 'videocontainer__toptoolbar'>
{ this._renderTopIndicators() }
</div>
<div className = 'videocontainer__hoverOverlay' />
{ this._renderAvatar(styles.avatar) }
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
<span className = 'localvideomenu'>
<LocalVideoMenuTriggerButton
hidePopover = { this._hidePopover }
popoverVisible = { this.state.popoverVisible }
showPopover = { this._showPopover } />
</span>
</span>
);
}
_onCanPlay: Object => void;
/**
@ -971,40 +722,59 @@ class Thumbnail extends Component<Props, State> {
/**
* Renders a remote participant's 'thumbnail.
*
* @param {boolean} local - Whether or not it's the local participant.
* @returns {ReactElement}
*/
_renderRemoteParticipant() {
_renderParticipant(local = false) {
const {
_audioTrack,
_currentLayout,
_disableLocalVideoFlip,
_isMobile,
_isMobilePortrait,
_isScreenSharing,
_isTestModeEnabled,
_localFlipX,
_participant,
_startSilent,
_videoTrack,
_volume = 1
classes
} = this.props;
const { id } = _participant;
const { audioLevel } = this.state;
const { id } = _participant || {};
const { isHovered, popoverVisible } = this.state;
const styles = this._getStyles();
const containerClassName = this._getContainerClassName();
// hide volume when in silent mode
const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
let containerClassName = this._getContainerClassName();
const videoTrackClassName
= !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
const videoEventListeners = {};
if (local) {
if (_isMobilePortrait) {
styles.thumbnail.height = styles.thumbnail.width;
containerClassName = `${containerClassName} self-view-mobile-portrait`;
}
} else {
if (_videoTrack && _isTestModeEnabled) {
VIDEO_TEST_EVENTS.forEach(attribute => {
videoEventListeners[attribute] = this._onTestingEvent;
});
}
videoEventListeners.onCanPlay = this._onCanPlay;
}
const video = _videoTrack && <VideoTrack
className = { local ? videoTrackClassName : '' }
eventHandlers = { videoEventListeners }
id = { local ? 'localVideo_container' : `remoteVideo_${videoTrackId || ''}` }
muted = { local ? undefined : true }
style = { styles.video }
videoTrack = { _videoTrack } />;
return (
<span
className = { containerClassName }
id = { `participant_${id}` }
id = { local ? 'localVideoContainer' : `participant_${id}` }
{ ...(_isMobile
? {
onTouchEnd: this._onTouchEnd,
@ -1018,64 +788,50 @@ class Thumbnail extends Component<Props, State> {
}
) }
style = { styles.thumbnail }>
{
_videoTrack && <VideoTrack
eventHandlers = { videoEventListeners }
id = { `remoteVideo_${videoTrackId || ''}` }
muted = { true }
style = { styles.video }
videoTrack = { _videoTrack } />
}
<div className = 'videocontainer__background' />
<div className = 'videocontainer__toptoolbar'>
{ this._renderTopIndicators() }
{local
? <span id = 'localVideoWrapper'>{video}</span>
: video}
<div className = { classes.containerBackground } />
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsTopContainer,
_currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
) }>
<ThumbnailTopIndicators
currentLayout = { _currentLayout }
hidePopover = { this._hidePopover }
indicatorsClassName = { classes.indicatorsBackground }
isHovered = { isHovered }
local = { local }
participantId = { id }
popoverVisible = { popoverVisible }
showPopover = { this._showPopover } />
</div>
<div className = 'videocontainer__toolbar'>
<StatusIndicators participantID = { id } />
<div className = 'videocontainer__participant-name'>
<DisplayName
elementID = { `participant_${id}_name` }
participantID = { id } />
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsBottomContainer,
_currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
) }>
<ThumbnailBottomIndicators
className = { classes.indicatorsBackground }
currentLayout = { _currentLayout }
local = { local }
participantId = { id } />
</div>
</div>
<div className = 'videocontainer__hoverOverlay' />
{ this._renderAvatar(styles.avatar) }
{ !local && (
<div className = 'presence-label-container'>
<PresenceLabel
className = 'presence-label'
participantID = { id } />
</div>
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
<span className = 'remotevideomenu'>
<RemoteVideoMenuTriggerButton
hidePopover = { this._hidePopover }
initialVolumeValue = { _volume }
onVolumeChange = { onVolumeChange }
participantID = { id }
popoverVisible = { this.state.popoverVisible }
showPopover = { this._showPopover } />
</span>
)}
<ThumbnailAudioIndicator _audioTrack = { _audioTrack } />
<div className = 'active-speaker-indicator' />
</span>
);
}
_onVolumeChange: number => void;
/**
* Handles volume changes.
*
* @param {number} value - The new value for the volume.
* @returns {void}
*/
_onVolumeChange(value) {
const { _participant, dispatch } = this.props;
const { id } = _participant;
dispatch(setVolume(id, value));
}
/**
* Implements React's {@link Component#render()}.
*
@ -1092,14 +848,14 @@ class Thumbnail extends Component<Props, State> {
const { isFakeParticipant, local } = _participant;
if (local) {
return this._renderLocalParticipant();
return this._renderParticipant(true);
}
if (isFakeParticipant) {
return this._renderFakeParticipant();
}
return this._renderRemoteParticipant();
return this._renderParticipant();
}
}
@ -1118,7 +874,6 @@ function _mapStateToProps(state, ownProps): Object {
const id = participant?.id;
const isLocal = participant?.local ?? true;
const tracks = state['features/base/tracks'];
const { participantsVolume } = state['features/filmstrip'];
const _videoTrack = isLocal
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
const _audioTrack = isLocal
@ -1127,14 +882,12 @@ function _mapStateToProps(state, ownProps): Object {
let size = {};
let _isMobilePortrait = false;
const {
startSilent,
defaultLocalDisplayName,
disableLocalVideoFlip,
disableTileEnlargement,
iAmRecorder,
iAmSipGateway
} = state['features/base/config'];
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
const { localFlipX } = state['features/base/settings'];
const _isMobile = isMobileBrowser();
@ -1167,7 +920,6 @@ function _mapStateToProps(state, ownProps): Object {
break;
}
case LAYOUTS.TILE_VIEW: {
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
size = {
@ -1179,12 +931,7 @@ function _mapStateToProps(state, ownProps): Object {
}
return {
_allowEditing: !isNameReadOnly(state),
_audioTrack,
_connectionIndicatorAutoHideEnabled:
Boolean(state['features/base/config'].connectionIndicators?.autoHide ?? true),
_connectionIndicatorDisabled: _isMobile
|| Boolean(state['features/base/config'].connectionIndicators?.disabled),
_currentLayout,
_defaultLocalDisplayName: defaultLocalDisplayName,
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
@ -1198,15 +945,11 @@ function _mapStateToProps(state, ownProps): Object {
_isScreenSharing: _videoTrack?.videoType === 'desktop',
_isTestModeEnabled: isTestModeEnabled(state),
_isVideoPlayable: id && isVideoPlayable(state, id),
_indicatorIconSize: NORMAL,
_localFlipX: Boolean(localFlipX),
_participant: participant,
_participantCountMoreThan2: getParticipantCount(state) > 2,
_startSilent: Boolean(startSilent),
_videoTrack,
_volume: isLocal ? undefined : id ? participantsVolume[id] : undefined,
...size
};
}
export default connect(_mapStateToProps)(Thumbnail);
export default connect(_mapStateToProps)(withStyles(defaultStyles)(Thumbnail));

View File

@ -0,0 +1,47 @@
// @flow
import React, { useEffect, useState } from 'react';
import { AudioLevelIndicator } from '../../../audio-level-indicator';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
const JitsiTrackEvents = JitsiMeetJS.events.track;
type Props = {
/**
* The audio track related to the participant.
*/
_audioTrack: ?Object
}
const ThumbnailAudioIndicator = ({
_audioTrack
}: Props) => {
const [ audioLevel, setAudioLevel ] = useState(0);
useEffect(() => {
setAudioLevel(0);
if (_audioTrack) {
const { jitsiTrack } = _audioTrack;
jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, setAudioLevel);
}
return () => {
if (_audioTrack) {
const { jitsiTrack } = _audioTrack;
jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, setAudioLevel);
}
};
}, [ _audioTrack ]);
return (
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
);
};
export default ThumbnailAudioIndicator;

View File

@ -0,0 +1,84 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import { useSelector } from 'react-redux';
import { isNameReadOnly } from '../../../base/config/functions.any';
import DisplayName from '../../../display-name/components/web/DisplayName';
import { LAYOUTS } from '../../../video-layout';
import StatusIndicators from './StatusIndicators';
declare var interfaceConfig: Object;
type Props = {
/**
* The current layout of the filmstrip.
*/
currentLayout: string,
/**
* Class name for indicators container.
*/
className: string,
/**
* Whether or not the indicators are for the local participant.
*/
local: boolean,
/**
* Id of the participant for which the component is displayed.
*/
participantId: string
}
const useStyles = makeStyles(() => {
return {
nameContainer: {
display: 'flex',
overflow: 'hidden',
padding: '2px 0',
'&>div': {
display: 'flex',
overflow: 'hidden'
},
'&:first-child': {
marginLeft: '6px'
}
}
};
});
const ThumbnailBottomIndicators = ({
className,
currentLayout,
local,
participantId
}: Props) => {
const styles = useStyles();
const _allowEditing = !useSelector(isNameReadOnly);
const _defaultLocalDisplayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME;
return (<div className = { className }>
<StatusIndicators
audio = { true }
moderator = { true }
participantID = { participantId }
screenshare = { currentLayout === LAYOUTS.TILE_VIEW } />
<span className = { styles.nameContainer }>
<DisplayName
allowEditing = { local ? _allowEditing : false }
currentLayout = { currentLayout }
displayNameSuffix = { local ? _defaultLocalDisplayName : '' }
elementID = { local ? 'localDisplayName' : `participant_${participantId}_name` }
participantID = { participantId } />
</span>
</div>);
};
export default ThumbnailBottomIndicators;

View File

@ -0,0 +1,132 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React from 'react';
import { useSelector } from 'react-redux';
import { isMobileBrowser } from '../../../base/environment/utils';
import ConnectionIndicator from '../../../connection-indicator/components/web/ConnectionIndicator';
import { LAYOUTS } from '../../../video-layout';
import { STATS_POPOVER_POSITION } from '../../constants';
import { getIndicatorsTooltipPosition } from '../../functions.web';
import RaisedHandIndicator from './RaisedHandIndicator';
import StatusIndicators from './StatusIndicators';
import VideoMenuTriggerButton from './VideoMenuTriggerButton';
declare var interfaceConfig: Object;
type Props = {
/**
* The current layout of the filmstrip.
*/
currentLayout: string,
/**
* Hide popover callback.
*/
hidePopover: Function,
/**
* Class name for the status indicators container.
*/
indicatorsClassName: string,
/**
* Whether or not the thumbnail is hovered.
*/
isHovered: boolean,
/**
* Whether or not the indicators are for the local participant.
*/
local: boolean,
/**
* Id of the participant for which the component is displayed.
*/
participantId: string,
/**
* Whether popover is visible or not.
*/
popoverVisible: boolean,
/**
* Show popover callback.
*/
showPopover: Function
}
const useStyles = makeStyles(() => {
return {
container: {
display: 'flex',
'& > *:not(:last-child)': {
marginRight: '4px'
}
}
};
});
const ThumbnailTopIndicators = ({
currentLayout,
hidePopover,
indicatorsClassName,
isHovered,
local,
participantId,
popoverVisible,
showPopover
}: Props) => {
const styles = useStyles();
const _isMobile = isMobileBrowser();
const { NORMAL = 16 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
const _indicatorIconSize = NORMAL;
const _connectionIndicatorAutoHideEnabled = Boolean(
useSelector(state => state['features/base/config'].connectionIndicators?.autoHide) ?? true);
const _connectionIndicatorDisabled = _isMobile
|| Boolean(useSelector(state => state['features/base/config'].connectionIndicators?.disabled));
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
return (
<>
<div className = { styles.container }>
{!_connectionIndicatorDisabled
&& <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { true }
iconSize = { _indicatorIconSize }
participantId = { participantId }
statsPopoverPosition = { STATS_POPOVER_POSITION[currentLayout] } />
}
<RaisedHandIndicator
iconSize = { _indicatorIconSize }
participantId = { participantId }
tooltipPosition = { getIndicatorsTooltipPosition(currentLayout) } />
{currentLayout === LAYOUTS.TILE_VIEW && (
<div className = { clsx(indicatorsClassName, 'top-indicators') }>
<StatusIndicators
participantID = { participantId }
screenshare = { true } />
</div>
)}
</div>
<div className = { styles.container }>
<VideoMenuTriggerButton
hidePopover = { hidePopover }
local = { local }
participantId = { participantId }
popoverVisible = { popoverVisible }
showPopover = { showPopover }
visible = { isHovered } />
</div>
</>);
};
export default ThumbnailTopIndicators;

View File

@ -23,6 +23,11 @@ type Props = {
*/
_horizontalOffset: number,
/**
* Whether or not there is a pinned participant.
*/
_isAnyParticipantPinned: boolean,
/**
* The ID of the participant associated with the Thumbnail.
*/
@ -75,7 +80,7 @@ class ThumbnailWrapper extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _participantID, style, _horizontalOffset = 0, _disableSelfView } = this.props;
const { _participantID, style, _horizontalOffset = 0, _isAnyParticipantPinned, _disableSelfView } = this.props;
if (typeof _participantID !== 'string') {
return null;
@ -91,6 +96,7 @@ class ThumbnailWrapper extends Component<Props> {
return (
<Thumbnail
_isAnyParticipantPinned = { _isAnyParticipantPinned }
horizontalOffset = { _horizontalOffset }
key = { `remote_${_participantID}` }
participantID = { _participantID }
@ -109,6 +115,7 @@ class ThumbnailWrapper extends Component<Props> {
function _mapStateToProps(state, ownProps) {
const _currentLayout = getCurrentLayout(state);
const { remoteParticipants } = state['features/filmstrip'];
const { remote, local } = state['features/base/participants'];
const remoteParticipantsLength = remoteParticipants.length;
const { testing = {} } = state['features/base/config'];
const disableSelfView = getDisableSelfView(state);
@ -160,8 +167,11 @@ function _mapStateToProps(state, ownProps) {
return {};
}
const _isAnyParticipantPinned = Boolean([ ...remote ].find(([ , value ]) => value?.pinned) || local?.pinned);
return {
_participantID: remoteParticipants[index]
_participantID: remoteParticipants[index],
_isAnyParticipantPinned
};
}

View File

@ -0,0 +1,69 @@
// @flow
import React from 'react';
import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
type Props = {
/**
* Hide popover callback.
*/
hidePopover: Function,
/**
* Whether or not the button is for the local participant.
*/
local: boolean,
/**
* The id of the participant for which the button is.
*/
participantId?: string,
/**
* Whether popover is visible or not.
*/
popoverVisible: boolean,
/**
* Show popover callback.
*/
showPopover: Function,
/**
* Whether or not the component is visible.
*/
visible: boolean
}
// eslint-disable-next-line no-confusing-arrow
const VideoMenuTriggerButton = ({
hidePopover,
local,
participantId,
popoverVisible,
showPopover,
visible
}: Props) => local
? (
<span id = 'localvideomenu'>
<LocalVideoMenuTriggerButton
buttonVisible = { visible }
hidePopover = { hidePopover }
popoverVisible = { popoverVisible }
showPopover = { showPopover } />
</span>
)
: (
<span id = 'remotevideomenu'>
<RemoteVideoMenuTriggerButton
buttonVisible = { visible }
hidePopover = { hidePopover }
participantID = { participantId }
popoverVisible = { popoverVisible }
showPopover = { showPopover } />
</span>
);
export default VideoMenuTriggerButton;

View File

@ -1,43 +0,0 @@
/* @flow */
import React, { Component } from 'react';
import { IconCameraDisabled } from '../../../base/icons';
import { BaseIndicator } from '../../../base/react';
/**
* The type of the React {@code Component} props of {@link VideoMutedIndicator}.
*/
type Props = {
/**
* From which side of the indicator the tooltip should appear from.
*/
tooltipPosition: string
};
/**
* React {@code Component} for showing a video muted icon with a tooltip.
*
* @augments Component
*/
class VideoMutedIndicator extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<BaseIndicator
className = 'videoMuted toolbar-icon'
icon = { IconCameraDisabled }
iconId = 'camera-disabled'
iconSize = { 13 }
tooltipKey = 'videothumbnail.videomute'
tooltipPosition = { this.props.tooltipPosition } />
);
}
}
export default VideoMutedIndicator;

View File

@ -1,10 +1,8 @@
// @flow
export { default as AudioMutedIndicator } from './AudioMutedIndicator';
export { default as DominantSpeakerIndicator } from './DominantSpeakerIndicator';
export { default as Filmstrip } from './Filmstrip';
export { default as ModeratorIndicator } from './ModeratorIndicator';
export { default as RaisedHandIndicator } from './RaisedHandIndicator';
export { default as StatusIndicators } from './StatusIndicators';
export { default as VideoMutedIndicator } from './VideoMutedIndicator';
export { default as Thumbnail } from './Thumbnail';

View File

@ -1,6 +1,7 @@
// @flow
import { BoxModel } from '../base/styles';
import { LAYOUTS } from '../video-layout/constants';
/**
* The size (height and width) of the small (not tile view) thumbnails.
@ -96,33 +97,6 @@ export const DISPLAY_VIDEO = 0;
*/
export const DISPLAY_AVATAR = 1;
/**
* Display mode constant used when neither video nor avatar is being displayed
* on the small video. And we just show the display name.
*
* @type {number}
* @constant
*/
export const DISPLAY_BLACKNESS_WITH_NAME = 2;
/**
* Display mode constant used when video is displayed and display name
* at the same time.
*
* @type {number}
* @constant
*/
export const DISPLAY_VIDEO_WITH_NAME = 3;
/**
* Display mode constant used when neither video nor avatar is being displayed
* on the small video. And we just show the display name.
*
* @type {number}
* @constant
*/
export const DISPLAY_AVATAR_WITH_NAME = 4;
/**
* Maps the display modes to class name that will be applied on the thumbnail container.
*
@ -131,24 +105,7 @@ export const DISPLAY_AVATAR_WITH_NAME = 4;
*/
export const DISPLAY_MODE_TO_CLASS_NAME = [
'display-video',
'display-avatar-only',
'display-name-on-black',
'display-name-on-video',
'display-avatar-with-name'
];
/**
* Maps the display modes to string.
*
* @type {Array<string>}
* @constant
*/
export const DISPLAY_MODE_TO_STRING = [
'video',
'avatar',
'blackness-with-name',
'video-with-name',
'avatar-with-name'
'display-avatar-only'
];
/**
@ -165,6 +122,20 @@ export const TILE_VERTICAL_MARGIN = 4;
*/
export const TILE_HORIZONTAL_MARGIN = 4;
/**
* The vertical margin of the tile grid container.
*
* @type {number}
*/
export const TILE_VIEW_GRID_VERTICAL_MARGIN = 12;
/**
* The horizontal margin of the tile grid container.
*
* @type {number}
*/
export const TILE_VIEW_GRID_HORIZONTAL_MARGIN = 12;
/**
* The height of the whole toolbar.
*/
@ -238,3 +209,21 @@ export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
* @type {number}
*/
export const TILE_MARGIN = 10;
/**
* The popover position for the connection stats table.
*/
export const STATS_POPOVER_POSITION = {
[LAYOUTS.TILE_VIEW]: 'right-start',
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'left-start',
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'top-end'
};
/**
* The tooltip position for the indicators on the thumbnail.
*/
export const INDICATORS_TOOLTIP_POSITION = {
[LAYOUTS.TILE_VIEW]: 'right',
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'left',
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'top'
};

View File

@ -15,20 +15,21 @@ import {
isLocalTrackMuted,
isRemoteTrackMuted
} from '../base/tracks/functions';
import { LAYOUTS } from '../video-layout';
import {
ASPECT_RATIO_BREAKPOINT,
DISPLAY_AVATAR,
DISPLAY_AVATAR_WITH_NAME,
DISPLAY_BLACKNESS_WITH_NAME,
DISPLAY_VIDEO,
DISPLAY_VIDEO_WITH_NAME,
INDICATORS_TOOLTIP_POSITION,
SCROLL_SIZE,
SQUARE_TILE_ASPECT_RATIO,
STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
TILE_ASPECT_RATIO,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
TILE_VIEW_GRID_HORIZONTAL_MARGIN,
TILE_VIEW_GRID_VERTICAL_MARGIN,
VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
} from './constants';
@ -190,8 +191,8 @@ export function calculateThumbnailSizeForTileView({
aspectRatio = SQUARE_TILE_ASPECT_RATIO;
}
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN);
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN);
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN) - TILE_VIEW_GRID_HORIZONTAL_MARGIN;
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
const initialWidth = viewWidth / columns;
const initialHeight = viewHeight / minVisibleRows;
const aspectRatioHeight = initialWidth / aspectRatio;
@ -240,32 +241,75 @@ export function getVerticalFilmstripVisibleAreaWidth() {
/**
* Computes information that determine the display mode.
*
* @param {Object} input - Obejct containing all necessary information for determining the display mode for
* @param {Object} input - Object containing all necessary information for determining the display mode for
* the thumbnail.
* @returns {number} - One of <tt>DISPLAY_VIDEO</tt>, <tt>DISPLAY_AVATAR</tt> or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
* @returns {number} - One of <tt>DISPLAY_VIDEO</tt> or <tt>DISPLAY_AVATAR</tt>.
*/
export function computeDisplayMode(input: Object) {
export function computeDisplayModeFromInput(input: Object) {
const {
isAudioOnly,
isCurrentlyOnLargeVideo,
isScreenSharing,
canPlayEventReceived,
isHovered,
isRemoteParticipant,
tileViewActive
} = input;
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
if (!tileViewActive && isScreenSharing && isRemoteParticipant) {
return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
return DISPLAY_AVATAR;
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
// Display name is always and only displayed when user is on the stage
return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_VIDEO : DISPLAY_AVATAR;
} else if (adjustedIsVideoPlayable && !isAudioOnly) {
// check hovering and change state to video with name
return isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
return DISPLAY_VIDEO;
}
// check hovering and change state to avatar with name
return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
return DISPLAY_AVATAR;
}
/**
* Extracts information for props and state needed to compute the display mode.
*
* @param {Object} props - The Thumbnail component's props.
* @param {Object} state - The Thumbnail component's state.
* @returns {Object}
*/
export function getDisplayModeInput(props: Object, state: Object) {
const {
_currentLayout,
_isAudioOnly,
_isCurrentlyOnLargeVideo,
_isScreenSharing,
_isVideoPlayable,
_participant,
_videoTrack
} = props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
const { canPlayEventReceived } = state;
return {
isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
isAudioOnly: _isAudioOnly,
tileViewActive,
isVideoPlayable: _isVideoPlayable,
connectionStatus: _participant?.connectionStatus,
canPlayEventReceived,
videoStream: Boolean(_videoTrack),
isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
isScreenSharing: _isScreenSharing,
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
};
}
/**
* Gets the tooltip position for the thumbnail indicators.
*
* @param {string} currentLayout - The current layout of the app.
* @returns {string}
*/
export function getIndicatorsTooltipPosition(currentLayout: string) {
return INDICATORS_TOOLTIP_POSITION[currentLayout] || 'top';
}

View File

@ -1,87 +1,16 @@
// @flow
import { withStyles } from '@material-ui/styles';
import React, { Component } from 'react';
import { createBreakoutRoomsEvent, sendAnalytics } from '../../../analytics';
import { approveParticipant } from '../../../av-moderation/actions';
import { Avatar } from '../../../base/avatar';
import { ContextMenu, ContextMenuItemGroup } from '../../../base/components';
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
import { openDialog } from '../../../base/dialog';
import { isIosMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import {
IconCloseCircle,
IconCrown,
IconMessage,
IconMicDisabled,
IconMicrophone,
IconMuteEveryoneElse,
IconRingGroup,
IconShareVideo,
IconVideoOff
} from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import {
getLocalParticipant,
getParticipantByIdOrUndefined,
isLocalParticipantModerator,
isParticipantModerator
getParticipantByIdOrUndefined
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
import { sendParticipantToRoom } from '../../../breakout-rooms/actions';
import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
import { openChatById } from '../../../chat/actions';
import { setVolume } from '../../../filmstrip/actions.web';
import { stopSharedVideo } from '../../../shared-video/actions.any';
import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
import { VolumeSlider } from '../../../video-menu/components/web';
import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
import { isForceMuted } from '../../functions';
import ParticipantContextMenu from '../../../video-menu/components/web/ParticipantContextMenu';
type Props = {
/**
* Whether or not the participant is audio force muted.
*/
_isAudioForceMuted: boolean,
/**
* The id of the current room.
*/
_currentRoomId: String,
/**
* True if the local participant is moderator and false otherwise.
*/
_isLocalModerator: boolean,
/**
* True if the chat button is enabled and false otherwise.
*/
_isChatButtonEnabled: boolean,
/**
* True if the participant is moderator and false otherwise.
*/
_isParticipantModerator: boolean,
/**
* True if the participant is video muted and false otherwise.
*/
_isParticipantVideoMuted: boolean,
/**
* True if the participant is audio muted and false otherwise.
*/
_isParticipantAudioMuted: boolean,
/**
* Whether or not the participant is video force muted.
*/
_isVideoForceMuted: boolean,
/**
* Shared video local participant owner.
*/
@ -92,27 +21,11 @@ type Props = {
*/
_participant: Object,
/**
* Rooms reference.
*/
_rooms: Array<Object>,
/**
* A value between 0 and 1 indicating the volume of the participant's
* audio element.
*/
_volume: ?number,
/**
* Closes a drawer if open.
*/
closeDrawer: Function,
/**
* An object containing the CSS classes.
*/
classes?: {[ key: string]: string},
/**
* The dispatch function from redux.
*/
@ -124,11 +37,6 @@ type Props = {
*/
drawerParticipant: Object,
/**
* Callback used to open a confirmation dialog for audio muting.
*/
muteAudio: Function,
/**
* Target elements against which positioning calculations are made.
*/
@ -152,31 +60,7 @@ type Props = {
/**
* The ID of the participant.
*/
participantID?: string,
/**
* True if an overflow drawer should be displayed.
*/
overflowDrawer: boolean,
/**
* The translate function.
*/
t: Function
};
const styles = theme => {
return {
text: {
color: theme.palette.text02,
padding: '10px 16px',
height: '40px',
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
boxSizing: 'border-box'
}
};
participantID: string
};
/**
@ -184,166 +68,6 @@ const styles = theme => {
*/
class MeetingParticipantContextMenu extends Component<Props> {
/**
* Creates new instance of MeetingParticipantContextMenu.
*
* @param {Props} props - The props.
*/
constructor(props: Props) {
super(props);
this._getCurrentParticipantId = this._getCurrentParticipantId.bind(this);
this._onGrantModerator = this._onGrantModerator.bind(this);
this._onKick = this._onKick.bind(this);
this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
this._onMuteVideo = this._onMuteVideo.bind(this);
this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
this._onStopSharedVideo = this._onStopSharedVideo.bind(this);
this._onSendToRoom = this._onSendToRoom.bind(this);
this._onVolumeChange = this._onVolumeChange.bind(this);
this._onAskToUnmute = this._onAskToUnmute.bind(this);
}
_getCurrentParticipantId: () => string;
/**
* Returns the participant id for the item we want to operate.
*
* @returns {void}
*/
_getCurrentParticipantId() {
const { _participant, drawerParticipant, overflowDrawer } = this.props;
return overflowDrawer ? drawerParticipant?.participantID : _participant?.id;
}
_onGrantModerator: () => void;
/**
* Grant moderator permissions.
*
* @returns {void}
*/
_onGrantModerator() {
this.props.dispatch(openDialog(GrantModeratorDialog, {
participantID: this._getCurrentParticipantId()
}));
}
_onKick: () => void;
/**
* Kicks the participant.
*
* @returns {void}
*/
_onKick() {
this.props.dispatch(openDialog(KickRemoteParticipantDialog, {
participantID: this._getCurrentParticipantId()
}));
}
_onStopSharedVideo: () => void;
/**
* Stops shared video.
*
* @returns {void}
*/
_onStopSharedVideo() {
const { dispatch, onSelect } = this.props;
onSelect(true);
dispatch(stopSharedVideo());
}
_onMuteEveryoneElse: () => void;
/**
* Mutes everyone else.
*
* @returns {void}
*/
_onMuteEveryoneElse() {
this.props.dispatch(openDialog(MuteEveryoneDialog, {
exclude: [ this._getCurrentParticipantId() ]
}));
}
_onMuteVideo: () => void;
/**
* Mutes the video of the selected participant.
*
* @returns {void}
*/
_onMuteVideo() {
this.props.dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
participantID: this._getCurrentParticipantId()
}));
}
_onSendPrivateMessage: () => void;
/**
* Sends private message.
*
* @returns {void}
*/
_onSendPrivateMessage() {
const { dispatch } = this.props;
dispatch(openChatById(this._getCurrentParticipantId()));
}
_onSendToRoom: (room: Object) => void;
/**
* Sends a participant to a room.
*
* @param {Object} room - The room that the participant should be moved to.
* @returns {void}
*/
_onSendToRoom(room: Object) {
return () => {
const { _participant, dispatch, onSelect } = this.props;
onSelect(true);
sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room'));
dispatch(sendParticipantToRoom(_participant.id, room.id));
};
}
_onVolumeChange: (number) => void;
/**
* Handles volume changes.
*
* @param {number} value - The new value for the volume.
* @returns {void}
*/
_onVolumeChange(value) {
const { _participant, dispatch } = this.props;
const { id } = _participant;
dispatch(setVolume(id, value));
}
_onAskToUnmute: () => void;
/**
* Handles click on ask to unmute.
*
* @returns {void}
*/
_onAskToUnmute() {
const { _participant, dispatch } = this.props;
const { id } = _participant;
dispatch(approveParticipant(id));
}
/**
* Implements React's {@link Component#render()}.
*
@ -352,165 +76,31 @@ class MeetingParticipantContextMenu extends Component<Props> {
*/
render() {
const {
_isAudioForceMuted,
_currentRoomId,
_isLocalModerator,
_isChatButtonEnabled,
_isParticipantModerator,
_isParticipantVideoMuted,
_isParticipantAudioMuted,
_isVideoForceMuted,
_localVideoOwner,
_participant,
_rooms,
_volume = 1,
classes,
closeDrawer,
drawerParticipant,
offsetTarget,
onEnter,
onLeave,
onSelect,
overflowDrawer,
muteAudio,
t
onSelect
} = this.props;
if (!_participant) {
return null;
}
const showVolumeSlider = !isIosMobileBrowser()
&& overflowDrawer
&& typeof _volume === 'number'
&& !isNaN(_volume);
const fakeParticipantActions = [ {
accessibilityLabel: t('toolbar.stopSharedVideo'),
icon: IconShareVideo,
onClick: this._onStopSharedVideo,
text: t('toolbar.stopSharedVideo')
} ];
const moderatorActions1 = [
overflowDrawer && (_isAudioForceMuted || _isVideoForceMuted) ? {
accessibilityLabel: t(_isAudioForceMuted
? 'participantsPane.actions.askUnmute'
: 'participantsPane.actions.allowVideo'),
icon: IconMicrophone,
onClick: this._onAskToUnmute,
text: t(_isAudioForceMuted
? 'participantsPane.actions.askUnmute'
: 'participantsPane.actions.allowVideo')
} : null,
!_isParticipantAudioMuted && overflowDrawer ? {
accessibilityLabel: t('dialog.muteParticipantButton'),
icon: IconMicDisabled,
onClick: muteAudio(_participant),
text: t('dialog.muteParticipantButton')
} : null, {
accessibilityLabel: t('toolbar.accessibilityLabel.muteEveryoneElse'),
icon: IconMuteEveryoneElse,
onClick: this._onMuteEveryoneElse,
text: t('toolbar.accessibilityLabel.muteEveryoneElse')
},
_isParticipantVideoMuted ? null : {
accessibilityLabel: t('participantsPane.actions.stopVideo'),
icon: IconVideoOff,
onClick: this._onMuteVideo,
text: t('participantsPane.actions.stopVideo')
}
].filter(Boolean);
const moderatorActions2 = [
_isLocalModerator && !_isParticipantModerator ? {
accessibilityLabel: t('toolbar.accessibilityLabel.grantModerator'),
icon: IconCrown,
onClick: this._onGrantModerator,
text: t('toolbar.accessibilityLabel.grantModerator')
} : null,
_isLocalModerator ? {
accessibilityLabel: t('videothumbnail.kick'),
icon: IconCloseCircle,
onClick: this._onKick,
text: t('videothumbnail.kick')
} : null,
_isChatButtonEnabled ? {
accessibilityLabel: t('toolbar.accessibilityLabel.privateMessage'),
icon: IconMessage,
onClick: this._onSendPrivateMessage,
text: t('toolbar.accessibilityLabel.privateMessage')
} : null
].filter(Boolean);
const breakoutRoomActions = _rooms.map(room => {
if (room.id !== _currentRoomId) {
return {
accessibilityLabel: room.name || t('breakoutRooms.mainRoom'),
icon: IconRingGroup,
onClick: this._onSendToRoom(room),
text: room.name || t('breakoutRooms.mainRoom')
};
}
return null;
}
).filter(Boolean);
const actions
= _participant?.isFakeParticipant ? (
<>
{_localVideoOwner && (
<ContextMenuItemGroup
actions = { fakeParticipantActions } />
)}
</>
) : (
<>
{_isLocalModerator
&& <ContextMenuItemGroup actions = { moderatorActions1 } />
}
<ContextMenuItemGroup actions = { moderatorActions2 } />
{
_isLocalModerator && _rooms.length > 1
&& <ContextMenuItemGroup actions = { breakoutRoomActions } >
<div className = { classes && classes.text }>
{t('breakoutRooms.actions.sendToBreakoutRoom')}
</div>
</ContextMenuItemGroup>
}
{ showVolumeSlider
&& <ContextMenuItemGroup>
<VolumeSlider
initialValue = { _volume }
key = 'volume-slider'
onChange = { this._onVolumeChange } />
</ContextMenuItemGroup>
}
</>
);
return (
<ContextMenu
entity = { _participant }
isDrawerOpen = { drawerParticipant }
<ParticipantContextMenu
closeDrawer = { closeDrawer }
drawerParticipant = { drawerParticipant }
localVideoOwner = { _localVideoOwner }
offsetTarget = { offsetTarget }
onClick = { onSelect }
onDrawerClose = { closeDrawer }
onMouseEnter = { onEnter }
onMouseLeave = { onLeave }>
{overflowDrawer && <ContextMenuItemGroup
actions = { [ {
accessibilityLabel: drawerParticipant && drawerParticipant.displayName,
customIcon: <Avatar
participantId = { drawerParticipant && drawerParticipant.participantID }
size = { 20 } />,
text: drawerParticipant && drawerParticipant.displayName
} ] } />}
{actions}
</ContextMenu>
onEnter = { onEnter }
onLeave = { onLeave }
onSelect = { onSelect }
participant = { _participant }
thumbnailMenu = { false } />
);
}
}
@ -531,32 +121,10 @@ function _mapStateToProps(state, ownProps): Object {
const participant = getParticipantByIdOrUndefined(state,
overflowDrawer ? drawerParticipant?.participantID : participantID);
const _currentRoomId = getCurrentRoomId(state);
const _isLocalModerator = isLocalParticipantModerator(state);
const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
const _isParticipantVideoMuted = isParticipantVideoMuted(participant, state);
const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
const _isParticipantModerator = isParticipantModerator(participant);
const _rooms = Object.values(getBreakoutRooms(state));
const { participantsVolume } = state['features/filmstrip'];
const id = participant?.id;
const isLocal = participant?.local ?? true;
return {
_isAudioForceMuted: isForceMuted(participant, MEDIA_TYPE.AUDIO, state),
_currentRoomId,
_isLocalModerator,
_isChatButtonEnabled,
_isParticipantModerator,
_isParticipantVideoMuted,
_isParticipantAudioMuted,
_isVideoForceMuted: isForceMuted(participant, MEDIA_TYPE.VIDEO, state),
_localVideoOwner: Boolean(ownerId === localParticipantId),
_participant: participant,
_rooms,
_volume: isLocal ? undefined : id ? participantsVolume[id] : undefined
_participant: participant
};
}
export default translate(connect(_mapStateToProps)(withStyles(styles)(MeetingParticipantContextMenu)));
export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu));

View File

@ -8,7 +8,7 @@ import { Icon, IconRaisedHandHollow } from '../../../base/icons';
const useStyles = makeStyles(theme => {
return {
indicator: {
backgroundColor: theme.palette.warning02,
backgroundColor: theme.palette.warning01,
borderRadius: `${theme.shape.borderRadius / 2}px`,
height: '24px',
width: '24px'

View File

@ -95,11 +95,13 @@ export const VideoStateIcons = {
[MEDIA_STATE.FORCE_MUTED]: (
<Icon
color = '#E04757'
id = 'videoMuted'
size = { 16 }
src = { IconCameraEmptyDisabled } />
),
[MEDIA_STATE.MUTED]: (
<Icon
id = 'videoMuted'
size = { 16 }
src = { IconCameraEmptyDisabled } />
),

View File

@ -27,7 +27,7 @@ type Props = AbstractButtonProps & {
* every participant (except the local one).
*/
class MuteEveryonesVideoButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo';
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideoStream';
icon = IconMuteVideoEveryone;
label = 'toolbar.muteEveryonesVideo';
tooltip = 'toolbar.muteEveryonesVideo';

View File

@ -4,6 +4,7 @@ import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../analytics';
import { rejectParticipantAudio } from '../../av-moderation/actions';
import { IconMicDisabled } from '../../base/icons';
import { MEDIA_TYPE } from '../../base/media';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
@ -54,12 +55,13 @@ export default class AbstractMuteButton extends AbstractButton<Props, *> {
const { dispatch, participantID } = this.props;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'mute.button',
'mute',
{
'participant_id': participantID
}));
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
dispatch(rejectParticipantAudio(participantID));
}
/**

View File

@ -29,7 +29,7 @@ export type Props = AbstractButtonProps & {
* An abstract remote video menu button which disables the camera of all the other participants.
*/
export default class AbstractMuteEveryoneElsesVideoButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideo';
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideoStream';
icon = IconMuteVideoEveryone;
label = 'videothumbnail.domuteVideoOfOthers';

View File

@ -0,0 +1,49 @@
// @flow
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { approveParticipant } from '../../../av-moderation/actions';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { IconMicrophoneEmpty } from '../../../base/icons';
type Props = {
/**
* Whether or not the participant is audio force muted.
*/
isAudioForceMuted: boolean,
/**
* Whether or not the participant is video force muted.
*/
isVideoForceMuted: boolean,
/**
* The ID for the participant on which the button will act.
*/
participantID: string
}
const AskToUnmuteButton = ({ isAudioForceMuted, isVideoForceMuted, participantID }: Props) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const _onClick = useCallback(() => {
dispatch(approveParticipant(participantID));
}, [ participantID ]);
const text = isAudioForceMuted || !isVideoForceMuted
? t('participantsPane.actions.askUnmute')
: t('participantsPane.actions.allowVideo');
return (
<ContextMenuItem
accessibilityLabel = { text }
icon = { IconMicrophoneEmpty }
onClick = { _onClick }
text = { text } />
);
};
export default AskToUnmuteButton;

View File

@ -1,13 +1,12 @@
// @flow
import React, { useCallback } from 'react';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { IconInfo } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { renderConnectionStatus } from '../../actions.web';
import VideoMenuButton from './VideoMenuButton';
type Props = {
/**
@ -29,19 +28,19 @@ type Props = {
const ConnectionStatusButton = ({
dispatch,
participantId,
t
}: Props) => {
const onClick = useCallback(() => {
const onClick = useCallback(e => {
e.stopPropagation();
dispatch(renderConnectionStatus(true));
}, [ dispatch ]);
return (
<VideoMenuButton
buttonText = { t('videothumbnail.connectionInfo') }
<ContextMenuItem
accessibilityLabel = { t('videothumbnail.connectionInfo') }
icon = { IconInfo }
id = { `connstatus_${participantId}` }
onClick = { onClick } />
onClick = { onClick }
text = { t('videothumbnail.connectionInfo') } />
);
};

View File

@ -2,12 +2,11 @@
import React, { PureComponent } from 'react';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { updateSettings } from '../../../base/settings';
import VideoMenuButton from './VideoMenuButton';
/**
* The type of the React {@code Component} props of {@link FlipLocalVideoButton}.
*/
@ -18,6 +17,11 @@ type Props = {
*/
_localFlipX: boolean,
/**
* Button text class name.
*/
className: string,
/**
* The redux dispatch function.
*/
@ -61,15 +65,18 @@ class FlipLocalVideoButton extends PureComponent<Props> {
*/
render() {
const {
className,
t
} = this.props;
return (
<VideoMenuButton
buttonText = { t('videothumbnail.flip') }
displayClass = 'fliplink'
<ContextMenuItem
accessibilityLabel = { t('videothumbnail.flip') }
className = 'fliplink'
id = 'flipLocalVideoButton'
onClick = { this._onClick } />
onClick = { this._onClick }
text = { t('videothumbnail.flip') }
textClassName = { className } />
);
}

View File

@ -2,6 +2,7 @@
import React from 'react';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { IconCrown } from '../../../base/icons';
import { connect } from '../../../base/redux';
@ -10,8 +11,6 @@ import AbstractGrantModeratorButton, {
type Props
} from '../AbstractGrantModeratorButton';
import VideoMenuButton from './VideoMenuButton';
declare var interfaceConfig: Object;
/**
@ -37,20 +36,20 @@ class GrantModeratorButton extends AbstractGrantModeratorButton {
* @returns {ReactElement}
*/
render() {
const { participantID, t, visible } = this.props;
const { t, visible } = this.props;
if (!visible) {
return null;
}
return (
<VideoMenuButton
buttonText = { t('videothumbnail.grantModerator') }
displayClass = 'grantmoderatorlink'
<ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.grantModerator') }
className = 'grantmoderatorlink'
icon = { IconCrown }
id = { `grantmoderatorlink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
onClick = { this._handleClick }
text = { t('videothumbnail.grantModerator') } />
);
}

View File

@ -2,14 +2,13 @@
import React, { PureComponent } from 'react';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { updateSettings } from '../../../base/settings';
import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../../../notifications';
import { openSettingsDialog, SETTINGS_TABS } from '../../../settings';
import VideoMenuButton from './VideoMenuButton';
/**
* The type of the React {@code Component} props of {@link HideSelfViewVideoButton}.
*/
@ -25,6 +24,11 @@ type Props = {
*/
dispatch: Function,
/**
* Button text class name.
*/
className: string,
/**
* Click handler executed aside from the main action.
*/
@ -63,15 +67,18 @@ class HideSelfViewVideoButton extends PureComponent<Props> {
*/
render() {
const {
className,
t
} = this.props;
return (
<VideoMenuButton
buttonText = { t('videothumbnail.hideSelfView') }
displayClass = 'hideselflink'
id = 'hideselfviewbutton'
onClick = { this._onClick } />
<ContextMenuItem
accessibilityLabel = { t('videothumbnail.hideSelfView') }
className = 'hideselflink'
id = 'hideselfviewButton'
onClick = { this._onClick }
text = { t('videothumbnail.hideSelfView') }
textClassName = { className } />
);
}

View File

@ -2,15 +2,14 @@
import React from 'react';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { IconKick } from '../../../base/icons';
import { IconCloseCircle } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractKickButton, {
type Props
} from '../AbstractKickButton';
import VideoMenuButton from './VideoMenuButton';
/**
* Implements a React {@link Component} which displays a button for kicking out
* a participant from the conference.
@ -43,13 +42,14 @@ class KickButton extends AbstractKickButton {
const { participantID, t } = this.props;
return (
<VideoMenuButton
buttonText = { t('videothumbnail.kick') }
displayClass = 'kicklink'
icon = { IconKick }
<ContextMenuItem
accessibilityLabel = { t('videothumbnail.kick') }
className = 'kicklink'
icon = { IconCloseCircle }
id = { `ejectlink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
onClick = { this._handleClick }
text = { t('videothumbnail.kick') } />
);
}

View File

@ -1,11 +1,14 @@
// @flow
import { withStyles } from '@material-ui/styles';
import React, { Component } from 'react';
import { batch } from 'react-redux';
import ContextMenu from '../../../base/components/context-menu/ContextMenu';
import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { Icon, IconMenuThumb } from '../../../base/icons';
import { Icon, IconHorizontalPoints } from '../../../base/icons';
import {
getLocalParticipant
} from '../../../base/participants';
@ -20,8 +23,6 @@ import { renderConnectionStatus } from '../../actions.web';
import ConnectionStatusButton from './ConnectionStatusButton';
import FlipLocalVideoButton from './FlipLocalVideoButton';
import HideSelfViewVideoButton from './HideSelfViewVideoButton';
import VideoMenu from './VideoMenu';
/**
* The type of the React {@code Component} props of
@ -29,16 +30,21 @@ import VideoMenu from './VideoMenu';
*/
type Props = {
/**
* Whether or not the button should be visible.
*/
buttonVisible: boolean,
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* The redux dispatch function.
*/
dispatch: Function,
/**
* Gets a ref to the current component instance.
*/
getRef: Function,
/**
* Hides popover.
*/
@ -87,6 +93,29 @@ type Props = {
t: Function
};
const styles = theme => {
return {
triggerButton: {
backgroundColor: theme.palette.action01,
padding: '3px',
display: 'inline-block',
borderRadius: '4px'
},
contextMenu: {
position: 'relative',
marginTop: 0,
right: 'auto',
padding: '0',
minWidth: '200px'
},
flipText: {
marginLeft: '36px'
}
};
};
/**
* React Component for displaying an icon associated with opening the
* the video menu for the local participant.
@ -122,6 +151,8 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
_showConnectionInfo,
_overflowDrawer,
_showLocalVideoFlipButton,
buttonVisible,
classes,
hidePopover,
popoverVisible,
t
@ -130,13 +161,22 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
const content = _showConnectionInfo
? <ConnectionIndicatorContent participantId = { _localParticipantId } />
: (
<VideoMenu id = 'localVideoMenu'>
<FlipLocalVideoButton onClick = { hidePopover } />
<HideSelfViewVideoButton onClick = { hidePopover } />
<ContextMenu
className = { classes.contextMenu }
hidden = { false }
inDrawer = { _overflowDrawer }>
<ContextMenuItemGroup>
<FlipLocalVideoButton
className = { _overflowDrawer ? classes.flipText : '' }
onClick = { hidePopover } />
<HideSelfViewVideoButton
className = { _overflowDrawer ? classes.flipText : '' }
onClick = { hidePopover } />
{ isMobileBrowser()
&& <ConnectionStatusButton participantId = { _localParticipantId } />
}
</VideoMenu>
</ContextMenuItemGroup>
</ContextMenu>
);
return (
@ -149,14 +189,14 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
overflowDrawer = { _overflowDrawer }
position = { _menuPosition }
visible = { popoverVisible }>
{!_overflowDrawer && (
{!_overflowDrawer && buttonVisible && (
<span
className = 'popover-trigger local-video-menu-trigger'>
className = { classes.triggerButton }
role = 'button'>
{!isMobileBrowser() && <Icon
ariaLabel = { t('dialog.localUserControls') }
role = 'button'
size = '1.4em'
src = { IconMenuThumb }
size = { 18 }
src = { IconHorizontalPoints }
tabIndex = { 0 }
title = { t('dialog.localUserControls') } />
}
@ -221,10 +261,10 @@ function _mapStateToProps(state) {
_menuPosition = 'left-start';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
_menuPosition = 'left-end';
_menuPosition = 'left-start';
break;
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
_menuPosition = 'top';
_menuPosition = 'top-start';
break;
default:
_menuPosition = 'auto';
@ -239,4 +279,4 @@ function _mapStateToProps(state) {
};
}
export default translate(connect(_mapStateToProps)(LocalVideoMenuTriggerButton));
export default translate(connect(_mapStateToProps)(withStyles(styles)(LocalVideoMenuTriggerButton)));

View File

@ -2,15 +2,15 @@
import React from 'react';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { IconMicDisabled } from '../../../base/icons';
import { IconMicrophoneEmptySlash } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMuteButton, {
_mapStateToProps,
type Props
} from '../AbstractMuteButton';
import VideoMenuButton from './VideoMenuButton';
/**
* Implements a React {@link Component} which displays a button for audio muting
@ -41,23 +41,20 @@ class MuteButton extends AbstractMuteButton {
* @returns {ReactElement}
*/
render() {
const { _audioTrackMuted, participantID, t } = this.props;
const muteConfig = _audioTrackMuted ? {
translationKey: 'videothumbnail.muted',
muteClassName: 'mutelink disabled'
} : {
translationKey: 'videothumbnail.domute',
muteClassName: 'mutelink'
};
const { _audioTrackMuted, t } = this.props;
if (_audioTrackMuted) {
return null;
}
return (
<VideoMenuButton
buttonText = { t(muteConfig.translationKey) }
displayClass = { muteConfig.muteClassName }
icon = { IconMicDisabled }
id = { `mutelink_${participantID}` }
<ContextMenuItem
accessibilityLabel = { t('dialog.muteParticipantButton') }
className = 'mutelink'
icon = { IconMicrophoneEmptySlash }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
onClick = { this._handleClick }
text = { t('dialog.muteParticipantButton') } />
);
}

View File

@ -2,6 +2,7 @@
import React from 'react';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { IconMuteEveryoneElse } from '../../../base/icons';
import { connect } from '../../../base/redux';
@ -9,8 +10,6 @@ import AbstractMuteEveryoneElseButton, {
type Props
} from '../AbstractMuteEveryoneElseButton';
import VideoMenuButton from './VideoMenuButton';
/**
* Implements a React {@link Component} which displays a button for audio muting
* every participant in the conference except the one with the given
@ -35,16 +34,15 @@ class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton {
* @returns {ReactElement}
*/
render() {
const { participantID, t } = this.props;
const { t } = this.props;
return (
<VideoMenuButton
buttonText = { t('videothumbnail.domuteOthers') }
displayClass = { 'mutelink' }
<ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElse') }
icon = { IconMuteEveryoneElse }
id = { `mutelink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
onClick = { this._handleClick }
text = { t('videothumbnail.domuteOthers') } />
);
}

View File

@ -2,6 +2,7 @@
import React from 'react';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { IconMuteVideoEveryoneElse } from '../../../base/icons';
import { connect } from '../../../base/redux';
@ -9,8 +10,6 @@ import AbstractMuteEveryoneElsesVideoButton, {
type Props
} from '../AbstractMuteEveryoneElsesVideoButton';
import VideoMenuButton from './VideoMenuButton';
/**
* Implements a React {@link Component} which displays a button for audio muting
* every participant in the conference except the one with the given
@ -35,16 +34,15 @@ class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton
* @returns {ReactElement}
*/
render() {
const { participantID, t } = this.props;
const { t } = this.props;
return (
<VideoMenuButton
buttonText = { t('videothumbnail.domuteVideoOfOthers') }
displayClass = { 'mutelink' }
<ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElsesVideoStream') }
icon = { IconMuteVideoEveryoneElse }
id = { `mutelink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
onClick = { this._handleClick }
text = { t('videothumbnail.domuteVideoOfOthers') } />
);
}

View File

@ -2,16 +2,15 @@
import React from 'react';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { IconCameraDisabled } from '../../../base/icons';
import { IconVideoOff } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMuteVideoButton, {
_mapStateToProps,
type Props
} from '../AbstractMuteVideoButton';
import VideoMenuButton from './VideoMenuButton';
/**
* Implements a React {@link Component} which displays a button for disabling
* the camera of a participant in the conference.
@ -41,23 +40,20 @@ class MuteVideoButton extends AbstractMuteVideoButton {
* @returns {ReactElement}
*/
render() {
const { _videoTrackMuted, participantID, t } = this.props;
const muteConfig = _videoTrackMuted ? {
translationKey: 'videothumbnail.videoMuted',
muteClassName: 'mutevideolink disabled'
} : {
translationKey: 'videothumbnail.domuteVideo',
muteClassName: 'mutevideolink'
};
const { _videoTrackMuted, t } = this.props;
if (_videoTrackMuted) {
return null;
}
return (
<VideoMenuButton
buttonText = { t(muteConfig.translationKey) }
displayClass = { muteConfig.muteClassName }
icon = { IconCameraDisabled }
id = { `mutelink_${participantID}` }
<ContextMenuItem
accessibilityLabel = { t('participantsPane.actions.stopVideo') }
className = 'mutevideolink'
icon = { IconVideoOff }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
onClick = { this._handleClick }
text = { t('participantsPane.actions.stopVideo') } />
);
}

View File

@ -0,0 +1,332 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { Avatar } from '../../../base/avatar';
import ContextMenu from '../../../base/components/context-menu/ContextMenu';
import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup';
import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
import { IconShareVideo } from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
import { setVolume } from '../../../filmstrip/actions.web';
import { isForceMuted } from '../../../participants-pane/functions';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { stopSharedVideo } from '../../../shared-video/actions.any';
import { showOverflowDrawer } from '../../../toolbox/functions.web';
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
import SendToRoomButton from './SendToRoomButton';
import {
AskToUnmuteButton,
ConnectionStatusButton,
GrantModeratorButton,
MuteButton,
MuteEveryoneElseButton,
MuteEveryoneElsesVideoButton,
MuteVideoButton,
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,
VolumeSlider
} from './';
type Props = {
/**
* Class name for the context menu.
*/
className?: string,
/**
* Closes a drawer if open.
*/
closeDrawer?: Function,
/**
* The participant for which the drawer is open.
* It contains the displayName & participantID.
*/
drawerParticipant?: Object,
/**
* Shared video local participant owner.
*/
localVideoOwner?: boolean,
/**
* Target elements against which positioning calculations are made.
*/
offsetTarget?: HTMLElement,
/**
* Callback for the mouse entering the component.
*/
onEnter?: Function,
/**
* Callback for the mouse leaving the component.
*/
onLeave?: Function,
/**
* Callback for making a selection in the menu.
*/
onSelect: Function,
/**
* Participant reference.
*/
participant: Object,
/**
* The current state of the participant's remote control session.
*/
remoteControlState?: number,
/**
* Whether or not the menu is displayed in the thumbnail remote video menu.
*/
thumbnailMenu: ?boolean
}
const useStyles = makeStyles(theme => {
return {
text: {
color: theme.palette.text02,
padding: '10px 16px',
height: '40px',
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
boxSizing: 'border-box'
}
};
});
const ParticipantContextMenu = ({
className,
closeDrawer,
drawerParticipant,
localVideoOwner,
offsetTarget,
onEnter,
onLeave,
onSelect,
participant,
remoteControlState,
thumbnailMenu
}: Props) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const styles = useStyles();
const localParticipant = useSelector(getLocalParticipant);
const _isModerator = Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR);
const _isAudioForceMuted = useSelector(state =>
isForceMuted(participant, MEDIA_TYPE.AUDIO, state));
const _isVideoForceMuted = useSelector(state =>
isForceMuted(participant, MEDIA_TYPE.VIDEO, state));
const _overflowDrawer = useSelector(showOverflowDrawer);
const { remoteVideoMenu = {}, disableRemoteMute, startSilent }
= useSelector(state => state['features/base/config']);
const { disableKick, disableGrantModerator } = remoteVideoMenu;
const { participantsVolume } = useSelector(state => state['features/filmstrip']);
const _volume = (participant?.local ?? true ? undefined
: participant?.id ? participantsVolume[participant?.id] : undefined) || 1;
const _currentRoomId = useSelector(getCurrentRoomId);
const _rooms = Object.values(useSelector(getBreakoutRooms));
const _onVolumeChange = useCallback(value => {
dispatch(setVolume(participant.id, value));
}, [ setVolume, dispatch ]);
const clickHandler = useCallback(() => onSelect(true), [ onSelect ]);
const _onStopSharedVideo = useCallback(() => {
clickHandler();
dispatch(stopSharedVideo());
}, [ stopSharedVideo ]);
const _getCurrentParticipantId = useCallback(() => {
const drawer = _overflowDrawer && !thumbnailMenu;
return (drawer ? drawerParticipant?.participantID : participant?.id) ?? '';
}
, [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]);
const buttons = [];
const buttons2 = [];
const showVolumeSlider = !startSilent
&& !isIosMobileBrowser()
&& (_overflowDrawer || thumbnailMenu)
&& typeof _volume === 'number'
&& !isNaN(_volume);
const fakeParticipantActions = [ {
accessibilityLabel: t('toolbar.stopSharedVideo'),
icon: IconShareVideo,
onClick: _onStopSharedVideo,
text: t('toolbar.stopSharedVideo')
} ];
if (_isModerator) {
if (thumbnailMenu || _overflowDrawer) {
buttons.push(<AskToUnmuteButton
isAudioForceMuted = { _isAudioForceMuted }
isVideoForceMuted = { _isVideoForceMuted }
key = 'ask-unmute'
participantID = { _getCurrentParticipantId() } />
);
}
if (!disableRemoteMute) {
buttons.push(
<MuteButton
key = 'mute'
participantID = { _getCurrentParticipantId() } />
);
buttons.push(
<MuteEveryoneElseButton
key = 'mute-others'
participantID = { _getCurrentParticipantId() } />
);
buttons.push(
<MuteVideoButton
key = 'mute-video'
participantID = { _getCurrentParticipantId() } />
);
buttons.push(
<MuteEveryoneElsesVideoButton
key = 'mute-others-video'
participantID = { _getCurrentParticipantId() } />
);
}
if (!disableGrantModerator) {
buttons2.push(
<GrantModeratorButton
key = 'grant-moderator'
participantID = { _getCurrentParticipantId() } />
);
}
if (!disableKick) {
buttons2.push(
<KickButton
key = 'kick'
participantID = { _getCurrentParticipantId() } />
);
}
}
buttons2.push(
<PrivateMessageMenuButton
key = 'privateMessage'
participantID = { _getCurrentParticipantId() } />
);
if (thumbnailMenu && isMobileBrowser()) {
buttons2.push(
<ConnectionStatusButton
key = 'conn-status'
participantId = { _getCurrentParticipantId() } />
);
}
if (thumbnailMenu && remoteControlState) {
let onRemoteControlToggle = null;
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
onRemoteControlToggle = () => dispatch(stopController(true));
} else if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
onRemoteControlToggle = () => dispatch(requestRemoteControl(_getCurrentParticipantId()));
}
buttons2.push(
<RemoteControlButton
key = 'remote-control'
onClick = { onRemoteControlToggle }
participantID = { _getCurrentParticipantId() }
remoteControlState = { remoteControlState } />
);
}
const breakoutRoomsButtons = [];
if (!thumbnailMenu && _isModerator) {
_rooms.forEach((room: Object) => {
if (room.id !== _currentRoomId) {
breakoutRoomsButtons.push(
<SendToRoomButton
key = { room.id }
onClick = { clickHandler }
participantID = { _getCurrentParticipantId() }
room = { room } />
);
}
});
}
return (
<ContextMenu
className = { className }
entity = { participant }
hidden = { thumbnailMenu ? false : undefined }
inDrawer = { thumbnailMenu && _overflowDrawer }
isDrawerOpen = { drawerParticipant }
offsetTarget = { offsetTarget }
onClick = { onSelect }
onDrawerClose = { thumbnailMenu ? onSelect : closeDrawer }
onMouseEnter = { onEnter }
onMouseLeave = { onLeave }>
{!thumbnailMenu && _overflowDrawer && drawerParticipant && <ContextMenuItemGroup
actions = { [ {
accessibilityLabel: drawerParticipant.displayName,
customIcon: <Avatar
participantId = { drawerParticipant.participantID }
size = { 20 } />,
text: drawerParticipant.displayName
} ] } />}
{participant?.isFakeParticipant ? localVideoOwner && (
<ContextMenuItemGroup
actions = { fakeParticipantActions } />
) : (
<>
{buttons.length > 0 && (
<ContextMenuItemGroup>
{buttons}
</ContextMenuItemGroup>
)}
<ContextMenuItemGroup>
{buttons2}
</ContextMenuItemGroup>
{showVolumeSlider && (
<ContextMenuItemGroup>
<VolumeSlider
initialValue = { _volume }
key = 'volume-slider'
onChange = { _onVolumeChange } />
</ContextMenuItemGroup>
)}
{breakoutRoomsButtons.length > 0 && (
<ContextMenuItemGroup>
<div className = { styles.text }>
{t('breakoutRooms.actions.sendToBreakoutRoom')}
</div>
{breakoutRoomsButtons}
</ContextMenuItemGroup>
)}
</>
)}
</ContextMenu>
);
};
export default ParticipantContextMenu;

View File

@ -2,6 +2,7 @@
import React, { Component } from 'react';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { IconMessage } from '../../../base/icons';
import { connect } from '../../../base/redux';
@ -12,8 +13,6 @@ import {
} from '../../../chat/components/web/PrivateMessageButton';
import { isButtonEnabled } from '../../../toolbox/functions.web';
import VideoMenuButton from './VideoMenuButton';
declare var interfaceConfig: Object;
type Props = AbstractProps & {
@ -49,18 +48,18 @@ class PrivateMessageMenuButton extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { participantID, t, _hidden } = this.props;
const { t, _hidden } = this.props;
if (_hidden) {
return null;
}
return (
<VideoMenuButton
buttonText = { t('toolbar.privateMessage') }
<ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.privateMessage') }
icon = { IconMessage }
id = { `privmsglink_${participantID}` }
onClick = { this._onClick } />
onClick = { this._onClick }
text = { t('toolbar.privateMessage') } />
);
}

View File

@ -6,11 +6,10 @@ import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../../analytics';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { translate } from '../../../base/i18n';
import { IconRemoteControlStart, IconRemoteControlStop } from '../../../base/icons';
import VideoMenuButton from './VideoMenuButton';
// TODO: Move these enums into the store after further reactification of the
// non-react RemoteVideo component.
export const REMOTE_CONTROL_MENU_STATES = {
@ -76,19 +75,18 @@ class RemoteControlButton extends Component<Props> {
*/
render() {
const {
participantID,
remoteControlState,
t
} = this.props;
let className, icon;
let disabled = false, icon;
switch (remoteControlState) {
case REMOTE_CONTROL_MENU_STATES.NOT_STARTED:
icon = IconRemoteControlStart;
break;
case REMOTE_CONTROL_MENU_STATES.REQUESTING:
className = ' disabled';
disabled = true;
icon = IconRemoteControlStart;
break;
case REMOTE_CONTROL_MENU_STATES.STARTED:
@ -102,12 +100,13 @@ class RemoteControlButton extends Component<Props> {
}
return (
<VideoMenuButton
buttonText = { t('videothumbnail.remoteControl') }
displayClass = { className }
<ContextMenuItem
accessibilityLabel = { t('videothumbnail.remoteControl') }
className = 'kicklink'
disabled = { disabled }
icon = { icon }
id = { `remoteControl_${participantID}` }
onClick = { this._onClick } />
onClick = { this._onClick }
text = { t('videothumbnail.remoteControl') } />
);
}

View File

@ -1,39 +1,26 @@
// @flow
/* eslint-disable react/jsx-handler-names */
import { withStyles } from '@material-ui/styles';
import React, { Component } from 'react';
import { batch } from 'react-redux';
import ConnectionIndicatorContent from
'../../../../features/connection-indicator/components/web/ConnectionIndicatorContent';
import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { Icon, IconMenuThumb } from '../../../base/icons';
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
import { Icon, IconHorizontalPoints } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { renderConnectionStatus } from '../../actions.web';
import ConnectionStatusButton from './ConnectionStatusButton';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
import ParticipantContextMenu from './ParticipantContextMenu';
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
import {
GrantModeratorButton,
MuteButton,
MuteVideoButton,
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,
VideoMenu,
VolumeSlider
} from './';
declare var $: Object;
/**
@ -57,26 +44,6 @@ type Props = {
*/
showPopover: Function,
/**
* Whether or not to display the kick button.
*/
_disableKick: boolean,
/**
* Whether or not to display the remote mute buttons.
*/
_disableRemoteMute: Boolean,
/**
* Whether or not to display the grant moderator button.
*/
_disableGrantModerator: Boolean,
/**
* Whether or not the participant is a conference moderator.
*/
_isModerator: boolean,
/**
* The position relative to the trigger the remote menu should display
* from. Valid values are those supported by AtlasKit
@ -89,33 +56,31 @@ type Props = {
*/
_overflowDrawer: boolean,
/**
* Participant reference.
*/
_participant: Object,
/**
* The current state of the participant's remote control session.
*/
_remoteControlState: number,
/**
* Whether or not the button should be visible.
*/
buttonVisible: boolean,
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* The redux dispatch function.
*/
dispatch: Function,
/**
* Gets a ref to the current component instance.
*/
getRef: Function,
/**
* A value between 0 and 1 indicating the volume of the participant's
* audio element.
*/
initialVolumeValue: ?number,
/**
* Callback to invoke when changing the level of the participant's
* audio element.
*/
onVolumeChange: Function,
/**
* The ID for the participant on which the remote video menu will act.
*/
@ -137,6 +102,26 @@ type Props = {
t: Function
};
const styles = theme => {
return {
triggerButton: {
backgroundColor: theme.palette.action01,
padding: '3px',
display: 'inline-block',
borderRadius: '4px'
},
contextMenu: {
position: 'relative',
marginTop: 0,
right: 'auto',
padding: '0',
marginRight: '4px',
marginBottom: '4px'
}
};
};
/**
* React {@code Component} for displaying an icon associated with opening the
* the {@code VideoMenu}.
@ -169,6 +154,8 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
_overflowDrawer,
_showConnectionInfo,
_participantDisplayName,
buttonVisible,
classes,
participantID,
popoverVisible
} = this.props;
@ -190,13 +177,14 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
onPopoverOpen = { this._onPopoverOpen }
position = { this.props._menuPosition }
visible = { popoverVisible }>
{!_overflowDrawer && (
<span className = 'popover-trigger remote-video-menu-trigger'>
{!_overflowDrawer && buttonVisible && (
<span
className = { classes.triggerButton }
role = 'button'>
{!isMobileBrowser() && <Icon
ariaLabel = { this.props.t('dialog.remoteUserControls', { username }) }
role = 'button'
size = '1.4em'
src = { IconMenuThumb }
size = { 18 }
src = { IconHorizontalPoints }
tabIndex = { 0 }
title = { this.props.t('dialog.remoteUserControls', { username }) } />
}
@ -245,134 +233,17 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @returns {ReactElement}
*/
_renderRemoteVideoMenu() {
const {
_disableKick,
_disableRemoteMute,
_disableGrantModerator,
_isModerator,
dispatch,
initialVolumeValue,
onVolumeChange,
_remoteControlState,
participantID
} = this.props;
const { _participant, _remoteControlState, classes } = this.props;
const actions = [];
const buttons = [];
const showVolumeSlider = !isIosMobileBrowser()
&& onVolumeChange
&& typeof initialVolumeValue === 'number'
&& !isNaN(initialVolumeValue);
if (_isModerator) {
if (!_disableRemoteMute) {
buttons.push(
<MuteButton
key = 'mute'
participantID = { participantID } />
);
buttons.push(
<MuteEveryoneElseButton
key = 'mute-others'
participantID = { participantID } />
);
buttons.push(
<MuteVideoButton
key = 'mute-video'
participantID = { participantID } />
);
buttons.push(
<MuteEveryoneElsesVideoButton
key = 'mute-others-video'
participantID = { participantID } />
);
}
if (!_disableGrantModerator) {
buttons.push(
<GrantModeratorButton
key = 'grant-moderator'
participantID = { participantID } />
);
}
if (!_disableKick) {
buttons.push(
<KickButton
key = 'kick'
participantID = { participantID } />
);
}
}
if (_remoteControlState) {
let onRemoteControlToggle = null;
if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
onRemoteControlToggle = () => dispatch(stopController(true));
} else if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
onRemoteControlToggle = () => dispatch(requestRemoteControl(participantID));
}
buttons.push(
<RemoteControlButton
key = 'remote-control'
onClick = { onRemoteControlToggle }
participantID = { participantID }
remoteControlState = { _remoteControlState } />
);
}
buttons.push(
<PrivateMessageMenuButton
key = 'privateMessage'
participantID = { participantID } />
);
if (isMobileBrowser()) {
actions.push(
<ConnectionStatusButton
key = 'conn-status'
participantId = { participantID } />
);
}
if (showVolumeSlider) {
actions.push(
<VolumeSlider
initialValue = { initialVolumeValue }
key = 'volume-slider'
onChange = { onVolumeChange } />
);
}
if (buttons.length > 0 || actions.length > 0) {
return (
<VideoMenu id = { participantID }>
<>
{ buttons.length > 0
&& <li onClick = { this.props.hidePopover }>
<ul className = 'popupmenu__list'>
{ buttons }
</ul>
</li>
}
</>
<>
{ actions.length > 0
&& <li>
<ul className = 'popupmenu__list'>
{actions}
</ul>
</li>
}
</>
</VideoMenu>
<ParticipantContextMenu
className = { classes.contextMenu }
onSelect = { this._onPopoverClose }
participant = { _participant }
remoteControlState = { _remoteControlState }
thumbnailMenu = { true } />
);
}
return null;
}
}
/**
@ -385,9 +256,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
*/
function _mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
const localParticipant = getLocalParticipant(state);
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
const { disableKick, disableGrantModerator } = remoteVideoMenu;
let _remoteControlState = null;
const participant = getParticipantById(state, participantID);
const _participantDisplayName = participant?.name;
@ -428,17 +296,14 @@ function _mapStateToProps(state, ownProps) {
}
return {
_isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
_disableKick: Boolean(disableKick),
_disableRemoteMute: Boolean(disableRemoteMute),
_remoteControlState,
_menuPosition,
_overflowDrawer: overflowDrawer,
_participant: participant,
_participantDisplayName,
_disableGrantModerator: Boolean(disableGrantModerator),
_remoteControlState,
_showConnectionInfo: showConnectionInfo
};
}
export default translate(connect(_mapStateToProps)(RemoteVideoMenuTriggerButton));
/* eslint-enable react/jsx-handler-names */
export default translate(connect(_mapStateToProps)(
withStyles(styles)(RemoteVideoMenuTriggerButton)));

View File

@ -0,0 +1,50 @@
// @flow
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { createBreakoutRoomsEvent, sendAnalytics } from '../../../analytics';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { IconRingGroup } from '../../../base/icons';
import { sendParticipantToRoom } from '../../../breakout-rooms/actions';
type Props = {
/**
* Click handler.
*/
onClick: ?Function,
/**
* The ID for the participant on which the button will act.
*/
participantID: string,
/**
* The room to send the participant to.
*/
room: Object
}
const SendToRoomButton = ({ onClick, participantID, room }: Props) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const _onClick = useCallback(() => {
onClick && onClick();
sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room'));
dispatch(sendParticipantToRoom(participantID, room.id));
}, [ participantID, room ]);
const roomName = room.name || t('breakoutRooms.mainRoom');
return (
<ContextMenuItem
accessibilityLabel = { roomName }
icon = { IconRingGroup }
onClick = { _onClick }
text = { roomName } />
);
};
export default SendToRoomButton;

View File

@ -1,51 +0,0 @@
// @flow
import React from 'react';
/**
* The type of the React {@code Component} props of {@link VideoMenu}.
*/
type Props = {
/**
* The components to place as the body of the {@code VideoMenu}.
*/
children: React$Node,
/**
* The id attribute to be added to the component's DOM for retrieval when
* querying the DOM. Not used directly by the component.
*/
id: string
};
/**
* Click handler.
*
* @param {SyntheticEvent} event - The click event.
* @returns {void}
*/
function onClick(event) {
// If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
// needs to be stopped.
event.stopPropagation();
}
/**
* React {@code Component} responsible for displaying other components as a menu
* for manipulating participant state.
*
* @param {Props} props - The component's props.
* @returns {Component}
*/
export default function VideoMenu(props: Props) {
return (
<ul
className = 'popupmenu'
id = { props.id }
onClick = { onClick }>
{ props.children }
</ul>
);
}

View File

@ -1,111 +0,0 @@
/* @flow */
import React, { Component } from 'react';
import { Icon } from '../../../base/icons';
/**
* The type of the React {@code Component} props of
* {@link VideoMenuButton}.
*/
type Props = {
/**
* Text to display within the component that describes the onClick action.
*/
buttonText: string,
/**
* Additional CSS classes to add to the component.
*/
displayClass?: string,
/**
* The icon that will display within the component.
*/
icon?: Object,
/**
* The id attribute to be added to the component's DOM for retrieval when
* querying the DOM. Not used directly by the component.
*/
id: string,
/**
* Callback to invoke when the component is clicked.
*/
onClick: Function,
};
/**
* React {@code Component} for displaying an action in {@code VideoMenuButton}.
*
* @augments {Component}
*/
export default class VideoMenuButton extends Component<Props> {
/**
* Initializes a new {@code RemoteVideoMenuButton} instance.
*
* @param {*} props - The read-only properties with which the new instance
* is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onKeyPress = this._onKeyPress.bind(this);
}
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The synthetic event.
* @returns {void}
*/
_onKeyPress(e) {
if (this.props.onClick && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
this.props.onClick();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
buttonText,
displayClass,
icon,
id,
onClick
} = this.props;
const linkClassName = `popupmenu__link ${displayClass || ''}`;
return (
<li className = 'popupmenu__item'>
<a
aria-label = { buttonText ? buttonText : 'some thing' }
className = { linkClassName }
id = { id }
onClick = { onClick }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>
<span className = 'popupmenu__icon'>
{ icon && <Icon src = { icon } /> }
</span>
<span className = 'popupmenu__text'>
{ buttonText }
</span>
</a>
</li>
);
}
}

View File

@ -1,5 +1,7 @@
/* @flow */
import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
@ -11,6 +13,11 @@ import { VOLUME_SLIDER_SCALE } from '../../constants';
*/
type Props = {
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* The value of the audio slider should display at when the component first
* mounts. Changes will be stored in state. The value should be a number
@ -41,6 +48,43 @@ type State = {
volumeLevel: number
};
const styles = theme => {
return {
container: {
minHeight: '40px',
width: '100%',
boxSizing: 'border-box',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
padding: '0 5px',
'&:hover': {
backgroundColor: theme.palette.ui04
}
},
icon: {
minWidth: '20px',
padding: '5px',
position: 'relative'
},
sliderContainer: {
position: 'relative',
width: '100%',
paddingRight: '5px'
},
slider: {
position: 'absolute',
width: '100%',
top: '50%',
transform: 'translate(0, -50%)'
}
};
};
/**
* Implements a React {@link Component} which displays an input slider for
* adjusting the local volume of a remote participant.
@ -65,6 +109,16 @@ class VolumeSlider extends Component<Props, State> {
this._onVolumeChange = this._onVolumeChange.bind(this);
}
/**
* Click handler.
*
* @param {MouseEvent} e - Click event.
* @returns {void}
*/
_onClick(e) {
e.stopPropagation();
}
/**
* Implements React's {@link Component#render()}.
*
@ -72,20 +126,24 @@ class VolumeSlider extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { classes } = this.props;
return (
<li
<div
aria-label = { this.props.t('volumeSlider') }
className = 'popupmenu__item'>
<div className = 'popupmenu__contents'>
<span className = 'popupmenu__icon'>
<Icon src = { IconVolume } />
className = { clsx('popupmenu__contents', classes.container) }
onClick = { this._onClick }>
<span className = { classes.icon }>
<Icon
size = { 22 }
src = { IconVolume } />
</span>
<div className = 'popupmenu__slider_container'>
<div className = { classes.sliderContainer }>
<input
aria-valuemax = { VOLUME_SLIDER_SCALE }
aria-valuemin = { 0 }
aria-valuenow = { this.state.volumeLevel }
className = 'popupmenu__slider'
className = { clsx('popupmenu__volume-slider', classes.slider) }
max = { VOLUME_SLIDER_SCALE }
min = { 0 }
onChange = { this._onVolumeChange }
@ -94,7 +152,6 @@ class VolumeSlider extends Component<Props, State> {
value = { this.state.volumeLevel } />
</div>
</div>
</li>
);
}
@ -116,4 +173,4 @@ class VolumeSlider extends Component<Props, State> {
}
}
export default translate(VolumeSlider);
export default translate(withStyles(styles)(VolumeSlider));

View File

@ -1,5 +1,6 @@
// @flow
export { default as AskToUnmuteButton } from './AskToUnmuteButton';
export { default as ConnectionStatusButton } from './ConnectionStatusButton';
export { default as GrantModeratorButton } from './GrantModeratorButton';
export { default as GrantModeratorDialog } from './GrantModeratorDialog';
@ -14,7 +15,6 @@ export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVide
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
export { default as VideoMenu } from './VideoMenu';
export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
export { default as LocalVideoMenuTriggerButton } from './LocalVideoMenuTriggerButton';
export { default as VolumeSlider } from './VolumeSlider';