diff --git a/css/_atlaskit_overrides.scss b/css/_atlaskit_overrides.scss index 992eb5449..ba5f5dba0 100644 --- a/css/_atlaskit_overrides.scss +++ b/css/_atlaskit_overrides.scss @@ -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. diff --git a/css/_drawer.scss b/css/_drawer.scss index c4eaf8186..981826494 100644 --- a/css/_drawer.scss +++ b/css/_drawer.scss @@ -42,15 +42,6 @@ } } - .popupmenu { - margin: auto; - width: 100%; - } - - .popupmenu__item { - height: 48px; - } - &#{&} .overflow-menu { margin: auto; font-size: 1.2em; diff --git a/css/_popover.scss b/css/_popover.scss index 6e82fda94..ff9aed51f 100644 --- a/css/_popover.scss +++ b/css/_popover.scss @@ -43,10 +43,5 @@ .popover { margin: -16px -24px; - padding: 16px 24px; z-index: $popoverZ; } - -.padded-content { - padding: 4px 8px; -} diff --git a/css/_popup_menu.scss b/css/_popup_menu.scss index 5a2a7fdbc..cd319c0d6 100644 --- a/css/_popup_menu.scss +++ b/css/_popup_menu.scss @@ -2,122 +2,18 @@ * Initialize **/ -.popupmenu { - background-color: $menuBG; - border-radius: 3px; - list-style-type: none; - min-width: 150px; - text-align: left; - padding: 0px; - white-space: nowrap; +.popupmenu__contents { + .popupmenu__volume-slider { + &::-webkit-slider-runnable-track { + background-color: $popupSliderColor; + } - &__item { - list-style-type: none; - height: 35px; - } + &::-moz-range-track { + background-color: $popupSliderColor; + } - // 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%; - - &::-webkit-slider-runnable-track { - background-color: $popupSliderColor; - } - - &::-moz-range-track { - background-color: $popupSliderColor; - } - - &::-ms-fill-lower { - background-color: $popupSliderColor; - } + &::-ms-fill-lower { + background-color: $popupSliderColor; } } - } - - &__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; } diff --git a/css/_variables.scss b/css/_variables.scss index fd1d0aba9..84ea030e9 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -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; diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index d78a0f697..2fd87e309 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -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; } diff --git a/css/filmstrip/_small_video.scss b/css/filmstrip/_small_video.scss index 4f7e5ede2..bffd1a50c 100644 --- a/css/filmstrip/_small_video.scss +++ b/css/filmstrip/_small_video.scss @@ -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; diff --git a/css/filmstrip/_tile_view.scss b/css/filmstrip/_tile_view.scss index ed5c689f9..44bd177b9 100644 --- a/css/filmstrip/_tile_view.scss +++ b/css/filmstrip/_tile_view.scss @@ -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; -} diff --git a/css/filmstrip/_tile_view_overrides.scss b/css/filmstrip/_tile_view_overrides.scss index 658f48918..0342ac640 100644 --- a/css/filmstrip/_tile_view_overrides.scss +++ b/css/filmstrip/_tile_view_overrides.scss @@ -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; - } } diff --git a/css/filmstrip/_vertical_filmstrip_overrides.scss b/css/filmstrip/_vertical_filmstrip_overrides.scss index fc30fe641..3c70a69ab 100644 --- a/css/filmstrip/_vertical_filmstrip_overrides.scss +++ b/css/filmstrip/_vertical_filmstrip_overrides.scss @@ -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; } diff --git a/css/themes/_light.scss b/css/themes/_light.scss index 49854d8f5..35b0dcef2 100644 --- a/css/themes/_light.scss +++ b/css/themes/_light.scss @@ -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 diff --git a/interface_config.js b/interface_config.js index c3a76afb9..cf97296e5 100644 --- a/interface_config.js +++ b/interface_config.js @@ -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', diff --git a/lang/main.json b/lang/main.json index c26dba26d..7335bb4e5 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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", diff --git a/react/features/base/components/context-menu/ContextMenu.js b/react/features/base/components/context-menu/ContextMenu.js index 0aee6d45d..bf556d706 100644 --- a/react/features/base/components/context-menu/ContextMenu.js +++ b/react/features/base/components/context-menu/ContextMenu.js @@ -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 (
+ {children} +
); + } + return _overflowDrawer ? { + 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 ( +
+ {customIcon ? customIcon + : icon && } + {text} +
+ ); +}; + +export default ContextMenuItem; diff --git a/react/features/base/components/context-menu/ContextMenuItemGroup.js b/react/features/base/components/context-menu/ContextMenuItemGroup.js index eb6d37a09..1e59eb8ef 100644 --- a/react/features/base/components/context-menu/ContextMenuItemGroup.js +++ b/react/features/base/components/context-menu/ContextMenuItemGroup.js @@ -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 (
{children} - {actions && actions.map(({ accessibilityLabel, className, customIcon, id, icon, onClick, text }) => ( -
- {customIcon ? customIcon - : icon && } - {text} -
+ {actions && actions.map(actionProps => ( + ))}
); diff --git a/react/features/base/icons/svg/crown.svg b/react/features/base/icons/svg/crown.svg index c35d74923..22d7ec669 100644 --- a/react/features/base/icons/svg/crown.svg +++ b/react/features/base/icons/svg/crown.svg @@ -1,3 +1,3 @@ - - + + diff --git a/react/features/base/icons/svg/mute-everyone-else.svg b/react/features/base/icons/svg/mute-everyone-else.svg index 4c37c9161..b62881034 100644 --- a/react/features/base/icons/svg/mute-everyone-else.svg +++ b/react/features/base/icons/svg/mute-everyone-else.svg @@ -1,11 +1,4 @@ - - - - - - - - - - + + + diff --git a/react/features/base/icons/svg/share-desktop.svg b/react/features/base/icons/svg/share-desktop.svg old mode 100755 new mode 100644 index e85acaa07..164780964 --- a/react/features/base/icons/svg/share-desktop.svg +++ b/react/features/base/icons/svg/share-desktop.svg @@ -1,3 +1,3 @@ - - + + diff --git a/react/features/base/popover/components/Popover.web.js b/react/features/base/popover/components/Popover.web.js index e49b325d8..333ceed21 100644 --- a/react/features/base/popover/components/Popover.web.js +++ b/react/features/base/popover/components/Popover.web.js @@ -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. */ @@ -79,7 +73,7 @@ type State = { /** * The style to apply to the context menu in order to position it correctly. */ - contextMenuStyle: Object + contextMenuStyle: Object }; /** @@ -364,15 +358,11 @@ class Popover extends Component { * @returns {ReactElement} */ _renderContent() { - const { content, paddedContent } = this.props; - const className = clsx( - 'popover popupmenu', - paddedContent && 'padded-content' - ); + const { content } = this.props; return (
{ content } {!isMobileBrowser() && ( diff --git a/react/features/base/react/components/web/BaseIndicator.js b/react/features/base/react/components/web/BaseIndicator.js index 96a50fac3..8ec741264 100644 --- a/react/features/base/react/components/web/BaseIndicator.js +++ b/react/features/base/react/components/web/BaseIndicator.js @@ -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,66 +60,58 @@ 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 + * @returns {ReactElement} */ -class BaseIndicator extends Component { - /** - * Default values for {@code BaseIndicator} component's properties. - * - * @static - */ - static defaultProps = { - className: '', - id: '', - tooltipPosition: 'top' - }; +const BaseIndicator = ({ + className = '', + icon, + iconClassName, + iconId, + iconSize, + id = '', + t, + tooltipKey, + tooltipPosition = 'top' +}: Props) => { + const styles = useStyles(); + const style = {}; - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { - className, - icon, - iconClassName, - iconId, - iconSize, - id, - t, - tooltipKey, - tooltipPosition - } = this.props; - const iconContainerClassName = `indicator-icon-container ${className}`; - const style = {}; - - if (iconSize) { - style.fontSize = iconSize; - } - - return ( -
- - - - - -
- ); + if (iconSize) { + style.fontSize = iconSize; } -} + + return ( +
+ + + + + +
+ ); +}; export default translate(BaseIndicator); diff --git a/react/features/connection-indicator/components/web/ConnectionIndicator.js b/react/features/connection-indicator/components/web/ConnectionIndicator.js index 7a2756b40..14d109453 100644 --- a/react/features/connection-indicator/components/web/ConnectionIndicator.js +++ b/react/features/connection-indicator/components/web/ConnectionIndicator.js @@ -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 = [ { 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 { * @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 { return ( } @@ -173,7 +222,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator { 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 { * @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 { * @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 { return ( ); } - let iconWidth; let emptyIconWrapperClassName = 'connection_empty'; if (this.props._connectionStatus @@ -283,34 +332,16 @@ class ConnectionIndicator extends AbstractConnectionIndicator { // 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 [ - + return ( + - , - - - ]; + ); } _onShowPopover: () => void; @@ -332,19 +363,10 @@ class ConnectionIndicator extends AbstractConnectionIndicator { * @returns {ReactElement} */ _renderIndicator() { - const colorClass = this._getConnectionColorClass(); - const indicatorContainerClassNames - = `connection-indicator indicator ${colorClass}`; - return ( -
-
-
- { this._renderIcon() } -
-
+
+ {this._renderIcon()}
); } @@ -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))); diff --git a/react/features/connection-stats/components/ConnectionStatsTable.js b/react/features/connection-stats/components/ConnectionStatsTable.js index 110c2e3fd..7a26006e6 100644 --- a/react/features/connection-stats/components/ConnectionStatsTable.js +++ b/react/features/connection-stats/components/ConnectionStatsTable.js @@ -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,20 +197,25 @@ class ConnectionStatsTable extends Component { * @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 ( -
- { this._renderStatistics() } -
- { isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null} - { !disableShowMoreStats && this._renderShowMoreLink() } +
+ ); } @@ -839,4 +865,4 @@ function getStringFromArray(array) { return res; } -export default translate(ConnectionStatsTable); +export default translate(withStyles(styles)(ConnectionStatsTable)); diff --git a/react/features/display-name/components/web/DisplayName.js b/react/features/display-name/components/web/DisplayName.js index b71f12f1a..ec2a03913 100644 --- a/react/features/display-name/components/web/DisplayName.js +++ b/react/features/display-name/components/web/DisplayName.js @@ -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 { const { _nameToDisplay, allowEditing, + currentLayout, displayNameSuffix, + classes, elementID, t } = this.props; @@ -155,10 +194,11 @@ class DisplayName extends Component { return ( { } return ( - - { appendSuffix(_nameToDisplay, displayNameSuffix) } - + + + { appendSuffix(_nameToDisplay, displayNameSuffix) } + + ); } + /** + * 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 { * 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))); diff --git a/react/features/filmstrip/components/web/AudioMutedIndicator.js b/react/features/filmstrip/components/web/AudioMutedIndicator.js index e6fdb8af7..fdf17da32 100644 --- a/react/features/filmstrip/components/web/AudioMutedIndicator.js +++ b/react/features/filmstrip/components/web/AudioMutedIndicator.js @@ -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 { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - return ( - - ); - } -} +const AudioMutedIndicator = ({ tooltipPosition }: Props) => ( + +); export default AudioMutedIndicator; diff --git a/react/features/filmstrip/components/web/DominantSpeakerIndicator.js b/react/features/filmstrip/components/web/DominantSpeakerIndicator.js deleted file mode 100644 index bd576685f..000000000 --- a/react/features/filmstrip/components/web/DominantSpeakerIndicator.js +++ /dev/null @@ -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 { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - */ - render() { - return ( - - ); - } -} - -export default DominantSpeakerIndicator; diff --git a/react/features/filmstrip/components/web/ModeratorIndicator.js b/react/features/filmstrip/components/web/ModeratorIndicator.js index aa802d4ec..33c6817af 100644 --- a/react/features/filmstrip/components/web/ModeratorIndicator.js +++ b/react/features/filmstrip/components/web/ModeratorIndicator.js @@ -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 { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - return ( -
- -
- ); - } -} +const ModeratorIndicator = ({ tooltipPosition }: Props) => ( + +); export default ModeratorIndicator; diff --git a/react/features/filmstrip/components/web/RaisedHandIndicator.js b/react/features/filmstrip/components/web/RaisedHandIndicator.js index 848f81b7f..04dbaa267 100644 --- a/react/features/filmstrip/components/web/RaisedHandIndicator.js +++ b/react/features/filmstrip/components/web/RaisedHandIndicator.js @@ -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 { - /** - * Renders the platform specific indicator element. - * - * @returns {React$Element<*>} - */ - _renderIndicator() { - return ( - - ); - } -} +const RaisedHandIndicator = ({ + iconSize, + participantId, + tooltipPosition +}: Props) => { + const _raisedHand = hasRaisedHand(useSelector(state => + getParticipantById(state, participantId))); + const styles = useStyles(); -export default connect(_mapStateToProps)(RaisedHandIndicator); + if (!_raisedHand) { + return null; + } + + return ( +
+ +
+ ); +}; + +export default RaisedHandIndicator; diff --git a/react/features/filmstrip/components/web/ScreenShareIndicator.js b/react/features/filmstrip/components/web/ScreenShareIndicator.js index e22538dc6..c7515a0e1 100644 --- a/react/features/filmstrip/components/web/ScreenShareIndicator.js +++ b/react/features/filmstrip/components/web/ScreenShareIndicator.js @@ -23,10 +23,9 @@ type Props = { export default function ScreenShareIndicator(props: Props) { return ( ); diff --git a/react/features/filmstrip/components/web/StatusIndicators.js b/react/features/filmstrip/components/web/StatusIndicators.js index 387725f3c..7fd790097 100644 --- a/react/features/filmstrip/components/web/StatusIndicators.js +++ b/react/features/filmstrip/components/web/StatusIndicators.js @@ -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 { _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 ( -
- { _showAudioMutedIndicator ? : null } - { _showScreenShareIndicator ? : null } - { _showVideoMutedIndicator ? : null } - { _showModeratorIndicator ? : null } -
+ <> + { _showAudioMutedIndicator && } + { _showModeratorIndicator && } + { _showScreenShareIndicator && } + ); } } @@ -108,24 +90,21 @@ class StatusIndicators extends Component { * }} */ 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 }; } diff --git a/react/features/filmstrip/components/web/Thumbnail.js b/react/features/filmstrip/components/web/Thumbnail.js index b266401df..3a6aa0e4d 100644 --- a/react/features/filmstrip/components/web/Thumbnail.js +++ b/react/features/filmstrip/components/web/Thumbnail.js @@ -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. */ @@ -195,25 +154,10 @@ 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 { */ 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 { 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 { * @returns {void} */ componentDidMount() { - this._listenForAudioUpdates(); this._onDisplayModeChanged(); } @@ -333,12 +328,6 @@ class Thumbnail extends Component { * @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 { * @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 { 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 { 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 { 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 { 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 { * @returns {Object} - The styles for the thumbnail. */ _getStyles(): Object { - const { canPlayEventReceived } = this.state; const { _currentLayout, @@ -575,7 +471,7 @@ class Thumbnail extends Component { 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 { ); } - /** - * 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 ( -
- { !_connectionIndicatorDisabled - && - } - - { showDominantSpeaker && _participantCountMoreThan2 - && - } -
); - } - /** * Renders the avatar. * @@ -820,115 +655,31 @@ class Thumbnail extends Component { _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 (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) { - className += ' active-speaker'; - } - - if (_isHidden) { - className += ' hidden'; - } - - if (isRemoteParticipant && _isAudioOnly) { - className += ' audio-only'; + if (_currentLayout === LAYOUTS.TILE_VIEW) { + if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) { + className += ` ${classes.activeSpeaker} dominant-speaker`; + } + } else if (_isAnyParticipantPinned) { + if (_participant?.pinned) { + className += ` videoContainerFocused ${classes.activeSpeaker}`; + } + } 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 ( - -
- - - -
- -
- -
-
-
- { this._renderTopIndicators() } -
-
- { this._renderAvatar(styles.avatar) } - - - - - - - - - ); - } - _onCanPlay: Object => void; /** @@ -971,40 +722,59 @@ class Thumbnail extends Component { /** * 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 (_videoTrack && _isTestModeEnabled) { - VIDEO_TEST_EVENTS.forEach(attribute => { - videoEventListeners[attribute] = this._onTestingEvent; - }); + 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; } - videoEventListeners.onCanPlay = this._onCanPlay; + const video = _videoTrack && ; return ( { } ) } style = { styles.thumbnail }> - { - _videoTrack && - } -
-
- { this._renderTopIndicators() } + {local + ? {video} + : video} +
+
+
-
- -
- + +
+ { this._renderAvatar(styles.avatar) } + { !local && ( +
+
-
-
- { this._renderAvatar(styles.avatar) } -
- -
- - - - - - + )} + +
); } - _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 { 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)); diff --git a/react/features/filmstrip/components/web/ThumbnailAudioIndicator.js b/react/features/filmstrip/components/web/ThumbnailAudioIndicator.js new file mode 100644 index 000000000..e7d4717ee --- /dev/null +++ b/react/features/filmstrip/components/web/ThumbnailAudioIndicator.js @@ -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 ( + + + + ); +}; + +export default ThumbnailAudioIndicator; diff --git a/react/features/filmstrip/components/web/ThumbnailBottomIndicators.js b/react/features/filmstrip/components/web/ThumbnailBottomIndicators.js new file mode 100644 index 000000000..4b4c97f14 --- /dev/null +++ b/react/features/filmstrip/components/web/ThumbnailBottomIndicators.js @@ -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 (
+ + + + +
); +}; + +export default ThumbnailBottomIndicators; diff --git a/react/features/filmstrip/components/web/ThumbnailTopIndicators.js b/react/features/filmstrip/components/web/ThumbnailTopIndicators.js new file mode 100644 index 000000000..a79ae692e --- /dev/null +++ b/react/features/filmstrip/components/web/ThumbnailTopIndicators.js @@ -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 ( + <> +
+ {!_connectionIndicatorDisabled + && + } + + {currentLayout === LAYOUTS.TILE_VIEW && ( +
+ +
+ )} +
+
+ +
+ ); +}; + +export default ThumbnailTopIndicators; diff --git a/react/features/filmstrip/components/web/ThumbnailWrapper.js b/react/features/filmstrip/components/web/ThumbnailWrapper.js index d9655bb2d..0a23ed8eb 100644 --- a/react/features/filmstrip/components/web/ThumbnailWrapper.js +++ b/react/features/filmstrip/components/web/ThumbnailWrapper.js @@ -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 { * @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 { return ( { 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 }; } diff --git a/react/features/filmstrip/components/web/VideoMenuTriggerButton.js b/react/features/filmstrip/components/web/VideoMenuTriggerButton.js new file mode 100644 index 000000000..20d35af18 --- /dev/null +++ b/react/features/filmstrip/components/web/VideoMenuTriggerButton.js @@ -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 + ? ( + + + + ) + : ( + + + + ); + +export default VideoMenuTriggerButton; diff --git a/react/features/filmstrip/components/web/VideoMutedIndicator.js b/react/features/filmstrip/components/web/VideoMutedIndicator.js deleted file mode 100644 index 88c104d78..000000000 --- a/react/features/filmstrip/components/web/VideoMutedIndicator.js +++ /dev/null @@ -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 { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - */ - render() { - return ( - - ); - } -} - -export default VideoMutedIndicator; diff --git a/react/features/filmstrip/components/web/index.js b/react/features/filmstrip/components/web/index.js index 410fdcf2b..cbb17a39d 100644 --- a/react/features/filmstrip/components/web/index.js +++ b/react/features/filmstrip/components/web/index.js @@ -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'; diff --git a/react/features/filmstrip/constants.js b/react/features/filmstrip/constants.js index 4be05f119..32950ae6e 100644 --- a/react/features/filmstrip/constants.js +++ b/react/features/filmstrip/constants.js @@ -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} - * @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' +}; diff --git a/react/features/filmstrip/functions.web.js b/react/features/filmstrip/functions.web.js index d0c9ab757..1398ec7e7 100644 --- a/react/features/filmstrip/functions.web.js +++ b/react/features/filmstrip/functions.web.js @@ -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 DISPLAY_VIDEO, DISPLAY_AVATAR or DISPLAY_BLACKNESS_WITH_NAME. + * @returns {number} - One of DISPLAY_VIDEO or DISPLAY_AVATAR. */ -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'; } diff --git a/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js b/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js index 315f76376..a6f297107 100644 --- a/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js +++ b/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js @@ -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, - - /** - * 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 { - /** - * 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 { */ 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 && ( - - )} - - ) : ( - <> - {_isLocalModerator - && - } - - - - { - _isLocalModerator && _rooms.length > 1 - && -
- {t('breakoutRooms.actions.sendToBreakoutRoom')} -
-
- } - { showVolumeSlider - && - - - } - - ); - return ( - - {overflowDrawer && , - text: drawerParticipant && drawerParticipant.displayName - } ] } />} - {actions} - + 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)); diff --git a/react/features/participants-pane/components/web/RaisedHandIndicator.js b/react/features/participants-pane/components/web/RaisedHandIndicator.js index 491b0d447..41afa0762 100644 --- a/react/features/participants-pane/components/web/RaisedHandIndicator.js +++ b/react/features/participants-pane/components/web/RaisedHandIndicator.js @@ -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' diff --git a/react/features/participants-pane/constants.js b/react/features/participants-pane/constants.js index d865e372c..f1210cb70 100644 --- a/react/features/participants-pane/constants.js +++ b/react/features/participants-pane/constants.js @@ -95,11 +95,13 @@ export const VideoStateIcons = { [MEDIA_STATE.FORCE_MUTED]: ( ), [MEDIA_STATE.MUTED]: ( ), diff --git a/react/features/toolbox/components/MuteEveryonesVideoButton.js b/react/features/toolbox/components/MuteEveryonesVideoButton.js index c0f79a99d..4248bf2c8 100644 --- a/react/features/toolbox/components/MuteEveryonesVideoButton.js +++ b/react/features/toolbox/components/MuteEveryonesVideoButton.js @@ -27,7 +27,7 @@ type Props = AbstractButtonProps & { * every participant (except the local one). */ class MuteEveryonesVideoButton extends AbstractButton { - accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo'; + accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideoStream'; icon = IconMuteVideoEveryone; label = 'toolbar.muteEveryonesVideo'; tooltip = 'toolbar.muteEveryonesVideo'; diff --git a/react/features/video-menu/components/AbstractMuteButton.js b/react/features/video-menu/components/AbstractMuteButton.js index d7f8e81af..2074319b5 100644 --- a/react/features/video-menu/components/AbstractMuteButton.js +++ b/react/features/video-menu/components/AbstractMuteButton.js @@ -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 { const { dispatch, participantID } = this.props; sendAnalytics(createRemoteVideoMenuButtonEvent( - 'mute.button', + 'mute', { 'participant_id': participantID })); dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO)); + dispatch(rejectParticipantAudio(participantID)); } /** diff --git a/react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js b/react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js index 750f3a6d7..0265cc289 100644 --- a/react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js +++ b/react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js @@ -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 { - accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideo'; + accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideoStream'; icon = IconMuteVideoEveryone; label = 'videothumbnail.domuteVideoOfOthers'; diff --git a/react/features/video-menu/components/web/AskToUnmuteButton.js b/react/features/video-menu/components/web/AskToUnmuteButton.js new file mode 100644 index 000000000..b119b628e --- /dev/null +++ b/react/features/video-menu/components/web/AskToUnmuteButton.js @@ -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 ( + + ); +}; + +export default AskToUnmuteButton; diff --git a/react/features/video-menu/components/web/ConnectionStatusButton.js b/react/features/video-menu/components/web/ConnectionStatusButton.js index a60bcb00f..7f87fa58b 100644 --- a/react/features/video-menu/components/web/ConnectionStatusButton.js +++ b/react/features/video-menu/components/web/ConnectionStatusButton.js @@ -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 ( - + onClick = { onClick } + text = { t('videothumbnail.connectionInfo') } /> ); }; diff --git a/react/features/video-menu/components/web/FlipLocalVideoButton.js b/react/features/video-menu/components/web/FlipLocalVideoButton.js index 1f86d1500..e357f5d2f 100644 --- a/react/features/video-menu/components/web/FlipLocalVideoButton.js +++ b/react/features/video-menu/components/web/FlipLocalVideoButton.js @@ -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 { */ render() { const { + className, t } = this.props; return ( - + onClick = { this._onClick } + text = { t('videothumbnail.flip') } + textClassName = { className } /> ); } diff --git a/react/features/video-menu/components/web/GrantModeratorButton.js b/react/features/video-menu/components/web/GrantModeratorButton.js index a30b91089..ae3da5220 100644 --- a/react/features/video-menu/components/web/GrantModeratorButton.js +++ b/react/features/video-menu/components/web/GrantModeratorButton.js @@ -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 ( - + onClick = { this._handleClick } + text = { t('videothumbnail.grantModerator') } /> ); } diff --git a/react/features/video-menu/components/web/HideSelfViewVideoButton.js b/react/features/video-menu/components/web/HideSelfViewVideoButton.js index 7684c4f03..fa7bd6c89 100644 --- a/react/features/video-menu/components/web/HideSelfViewVideoButton.js +++ b/react/features/video-menu/components/web/HideSelfViewVideoButton.js @@ -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 { */ render() { const { + className, t } = this.props; return ( - + ); } diff --git a/react/features/video-menu/components/web/KickButton.js b/react/features/video-menu/components/web/KickButton.js index 17fa8fd80..dbd5c8204 100644 --- a/react/features/video-menu/components/web/KickButton.js +++ b/react/features/video-menu/components/web/KickButton.js @@ -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 ( - + onClick = { this._handleClick } + text = { t('videothumbnail.kick') } /> ); } diff --git a/react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js b/react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js index 5b5b29b50..6aa2a2905 100644 --- a/react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js +++ b/react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js @@ -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 @@ -30,29 +31,34 @@ import VideoMenu from './VideoMenu'; type Props = { /** - * The redux dispatch function. + * Whether or not the button should be visible. */ - dispatch: Function, + buttonVisible: boolean, /** - * Gets a ref to the current component instance. + * An object containing the CSS classes. */ - getRef: Function, + classes: Object, + + /** + * The redux dispatch function. + */ + dispatch: Function, /** * Hides popover. */ - hidePopover: Function, + hidePopover: Function, /** * Whether the popover is visible or not. */ - popoverVisible: boolean, + popoverVisible: boolean, /** * Shows popover. */ - showPopover: Function, + showPopover: Function, /** * The id of the local participant. @@ -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 { _showConnectionInfo, _overflowDrawer, _showLocalVideoFlipButton, + buttonVisible, + classes, hidePopover, popoverVisible, t @@ -130,13 +161,22 @@ class LocalVideoMenuTriggerButton extends Component { const content = _showConnectionInfo ? : ( - - - - { isMobileBrowser() - && - } - + ); return ( @@ -149,14 +189,14 @@ class LocalVideoMenuTriggerButton extends Component { overflowDrawer = { _overflowDrawer } position = { _menuPosition } visible = { popoverVisible }> - {!_overflowDrawer && ( + {!_overflowDrawer && buttonVisible && ( + className = { classes.triggerButton } + role = 'button'> {!isMobileBrowser() && } @@ -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))); diff --git a/react/features/video-menu/components/web/MuteButton.js b/react/features/video-menu/components/web/MuteButton.js index 76f07da26..40b2e2b12 100644 --- a/react/features/video-menu/components/web/MuteButton.js +++ b/react/features/video-menu/components/web/MuteButton.js @@ -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 ( - + onClick = { this._handleClick } + text = { t('dialog.muteParticipantButton') } /> ); } diff --git a/react/features/video-menu/components/web/MuteEveryoneElseButton.js b/react/features/video-menu/components/web/MuteEveryoneElseButton.js index f62608efd..4b51545b7 100644 --- a/react/features/video-menu/components/web/MuteEveryoneElseButton.js +++ b/react/features/video-menu/components/web/MuteEveryoneElseButton.js @@ -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 ( - + onClick = { this._handleClick } + text = { t('videothumbnail.domuteOthers') } /> ); } diff --git a/react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js b/react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js index 165a69fb9..6a264d516 100644 --- a/react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js +++ b/react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js @@ -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 ( - + onClick = { this._handleClick } + text = { t('videothumbnail.domuteVideoOfOthers') } /> ); } diff --git a/react/features/video-menu/components/web/MuteVideoButton.js b/react/features/video-menu/components/web/MuteVideoButton.js index 5a688ce33..864906964 100644 --- a/react/features/video-menu/components/web/MuteVideoButton.js +++ b/react/features/video-menu/components/web/MuteVideoButton.js @@ -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 ( - + onClick = { this._handleClick } + text = { t('participantsPane.actions.stopVideo') } /> ); } diff --git a/react/features/video-menu/components/web/ParticipantContextMenu.js b/react/features/video-menu/components/web/ParticipantContextMenu.js new file mode 100644 index 000000000..edf97bed4 --- /dev/null +++ b/react/features/video-menu/components/web/ParticipantContextMenu.js @@ -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( + ); + } + if (!disableRemoteMute) { + buttons.push( + + ); + buttons.push( + + ); + buttons.push( + + ); + buttons.push( + + ); + } + + if (!disableGrantModerator) { + buttons2.push( + + ); + } + + if (!disableKick) { + buttons2.push( + + ); + } + } + + buttons2.push( + + ); + + if (thumbnailMenu && isMobileBrowser()) { + buttons2.push( + + ); + } + + 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( + + ); + } + + const breakoutRoomsButtons = []; + + if (!thumbnailMenu && _isModerator) { + _rooms.forEach((room: Object) => { + if (room.id !== _currentRoomId) { + breakoutRoomsButtons.push( + + ); + } + }); + } + + return ( + + ); +}; + +export default ParticipantContextMenu; diff --git a/react/features/video-menu/components/web/PrivateMessageMenuButton.js b/react/features/video-menu/components/web/PrivateMessageMenuButton.js index 4d7270518..3a58e0afe 100644 --- a/react/features/video-menu/components/web/PrivateMessageMenuButton.js +++ b/react/features/video-menu/components/web/PrivateMessageMenuButton.js @@ -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 { * @returns {ReactElement} */ render() { - const { participantID, t, _hidden } = this.props; + const { t, _hidden } = this.props; if (_hidden) { return null; } return ( - + onClick = { this._onClick } + text = { t('toolbar.privateMessage') } /> ); } diff --git a/react/features/video-menu/components/web/RemoteControlButton.js b/react/features/video-menu/components/web/RemoteControlButton.js index 9602d0d23..31c5e4702 100644 --- a/react/features/video-menu/components/web/RemoteControlButton.js +++ b/react/features/video-menu/components/web/RemoteControlButton.js @@ -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 { */ 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 { } return ( - + onClick = { this._onClick } + text = { t('videothumbnail.remoteControl') } /> ); } diff --git a/react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js b/react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js index 2e77a937f..8b73c9c55 100644 --- a/react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js +++ b/react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js @@ -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; /** @@ -45,37 +32,17 @@ type Props = { /** * Hides popover. */ - hidePopover: Function, + hidePopover: Function, /** * Whether the popover is visible or not. */ - popoverVisible: boolean, + popoverVisible: boolean, /** * Shows popover. */ - 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, + showPopover: Function, /** * The position relative to the trigger the remote menu should display @@ -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 { _overflowDrawer, _showConnectionInfo, _participantDisplayName, + buttonVisible, + classes, participantID, popoverVisible } = this.props; @@ -190,13 +177,14 @@ class RemoteVideoMenuTriggerButton extends Component { onPopoverOpen = { this._onPopoverOpen } position = { this.props._menuPosition } visible = { popoverVisible }> - {!_overflowDrawer && ( - + {!_overflowDrawer && buttonVisible && ( + {!isMobileBrowser() && } @@ -245,133 +233,16 @@ class RemoteVideoMenuTriggerButton extends Component { * @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( - - ); - buttons.push( - - ); - buttons.push( - - ); - buttons.push( - - ); - } - - if (!_disableGrantModerator) { - buttons.push( - - ); - } - - if (!_disableKick) { - buttons.push( - - ); - } - } - - 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( - - ); - } - - buttons.push( - + return ( + ); - - if (isMobileBrowser()) { - actions.push( - - ); - } - - if (showVolumeSlider) { - actions.push( - - ); - } - - if (buttons.length > 0 || actions.length > 0) { - return ( - - <> - { buttons.length > 0 - &&
  • -
      - { buttons } -
    -
  • - } - - <> - { actions.length > 0 - &&
  • -
      - {actions} -
    -
  • - } - -
    - ); - } - - return null; } } @@ -385,9 +256,6 @@ class RemoteVideoMenuTriggerButton extends Component { */ 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))); diff --git a/react/features/video-menu/components/web/SendToRoomButton.js b/react/features/video-menu/components/web/SendToRoomButton.js new file mode 100644 index 000000000..a1f100523 --- /dev/null +++ b/react/features/video-menu/components/web/SendToRoomButton.js @@ -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 ( + + ); +}; + +export default SendToRoomButton; diff --git a/react/features/video-menu/components/web/VideoMenu.js b/react/features/video-menu/components/web/VideoMenu.js deleted file mode 100644 index 6924428f3..000000000 --- a/react/features/video-menu/components/web/VideoMenu.js +++ /dev/null @@ -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 ( -
      - { props.children } -
    - ); -} - diff --git a/react/features/video-menu/components/web/VideoMenuButton.js b/react/features/video-menu/components/web/VideoMenuButton.js deleted file mode 100644 index 2e32ad3c0..000000000 --- a/react/features/video-menu/components/web/VideoMenuButton.js +++ /dev/null @@ -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 { - /** - * 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 ( -
  • - - - { icon && } - - - { buttonText } - - -
  • - ); - } -} diff --git a/react/features/video-menu/components/web/VolumeSlider.js b/react/features/video-menu/components/web/VolumeSlider.js index 92e6564b0..4198bb768 100644 --- a/react/features/video-menu/components/web/VolumeSlider.js +++ b/react/features/video-menu/components/web/VolumeSlider.js @@ -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 { 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,29 +126,32 @@ class VolumeSlider extends Component { * @returns {ReactElement} */ render() { + const { classes } = this.props; + return ( -
  • -
    - - - -
    - -
    + className = { clsx('popupmenu__contents', classes.container) } + onClick = { this._onClick }> + + + +
    +
    -
  • + ); } @@ -116,4 +173,4 @@ class VolumeSlider extends Component { } } -export default translate(VolumeSlider); +export default translate(withStyles(styles)(VolumeSlider)); diff --git a/react/features/video-menu/components/web/index.js b/react/features/video-menu/components/web/index.js index a700adfa6..a258e02e0 100644 --- a/react/features/video-menu/components/web/index.js +++ b/react/features/video-menu/components/web/index.js @@ -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';